fix(triggers): support multiple subscriptions per (connection, type) and cascade on connection delete#3319
Open
viktormarinho wants to merge 2 commits intomainfrom
Open
fix(triggers): support multiple subscriptions per (connection, type) and cascade on connection delete#3319viktormarinho wants to merge 2 commits intomainfrom
viktormarinho wants to merge 2 commits intomainfrom
Conversation
The TriggerStateManager keyed callback credentials by connectionId, so two TRIGGER_CONFIGURE calls on the same (connectionId, type) overwrote each other's tokens and one disable wiped the entire connection's state. This made it impossible to host independent subscriptions on a shared OAuth connection — e.g. two automations listening to gmail.message.received with different filter params. Add an optional `subscriptionId` to TRIGGER_CONFIGURE input. When present, it disambiguates registrations on the same connection; when absent it collapses to a single legacy slot so older mesh versions keep working. The TriggerStorage interface gains a `list(connectionId)` method so notify can fanout to every active subscription. Both built-in storages (StudioKV, JsonFileStorage) implement it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… on connection delete Each automation trigger now lives as its own row in `trigger_callback_tokens`, keyed on the trigger id. Previously the table was constrained to one row per (connection_id, organization_id) and re-running TRIGGER_CONFIGURE for a sibling subscription overwrote the prior token, leaving older triggers with a stale callback the MCP could no longer authenticate. The `id` of `automation_triggers` is now passed to TRIGGER_CONFIGURE as `subscriptionId` so the MCP can store independent records per registration. `trigger-add` pre-allocates the uuid before calling the MCP so the configure call and the DB insert agree on the same id. `connection/delete` now disables every event trigger bound to the connection on its MCP before removing the connection row. Without this cascade the MCP retained orphaned subscription state and continued trying to deliver to invalidated callback tokens. Migration 077 relaxes the unique index from (connection_id, organization_id) to (subscription_id), keeping a non-unique lookup index on (connection_id, organization_id) for fanout queries. Existing rows are backfilled with subscription_id = id; they remain valid until the next TRIGGER_CONFIGURE replaces them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
🧪 BenchmarkShould we run the Virtual MCP strategy benchmark for this PR? React with 👍 to run the benchmark.
Benchmark will run on the next push after you react. |
Contributor
Release OptionsSuggested: Patch ( React with an emoji to override the release type:
Current version:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The trigger registration system collapsed multiple subscriptions on the same
(connection, event_type)into a single slot, overwriting credentials and wiping callbacks when one of them was disabled. Connection deletion also left orphan state on MCPs because mesh never told the MCP a subscription should disappear.This PR carries three fixes:
TRIGGER_CONFIGUREgains an optionalsubscriptionId. The runtime'sTriggerStateManagerkeys credentials by(connectionId, subscriptionId)instead ofconnectionIdalone, and theTriggerStorageinterface gains alist(connectionId)method sonotifycan fanout to every subscription on a connection. The legacy__defaultslot keeps single-sub MCPs working unchanged.trigger_callback_tokensgets asubscription_idcolumn (=automation_triggers.id). The unique index moves from(connection_id, organization_id)tosubscription_id, with a non-unique lookup index left on the old pair.persistTokenHash/deleteBySubscription/listByConnectionall key on the new column. Migration 077 backfills existing rows and ships the index swap.trigger-addpre-allocates the trigger id before calling the MCP, then reuses it on the DB insert, so configure and persistence agree on the samesubscriptionId.automation/deletenow passestokenStorageso per-subscription tokens are cleaned up.connection/deleterunsTRIGGER_CONFIGURE(enabled=false)on every event trigger bound to the connection before the row is removed — this stops the orphan accumulation we observed in the gmail MCP, where deleted-and-recreated connections left deadtriggers:<connId>records in KV.Why
Studio assigns one
connectionIdper OAuth credential and reuses it across automations. Each automation registers its own trigger with a fresh callback token. The previous runtime stored only the latest token, so:disableremoved the entire connection record onceactiveTriggerTypesreached zero.triggers:<connId>orphans that fired "nobody subscribed" on every webhook delivery for that email afterwards.After this PR the MCP can hold N independent subscriptions per connection, mesh tracks each separately end-to-end, and connection deletion is a clean cascade.
Schema migration
077-trigger-callback-tokens-per-subscription.tsaddssubscription_id, backfills existing rows withsubscription_id = id(self-reference; rows stay validatable until the next TRIGGER_CONFIGURE writes a fresh id-keyed row), drops the old unique index on(connection_id, organization_id), and createsidx_trigger_callback_tokens_subscription. A non-uniqueidx_trigger_callback_tokens_connection_org_lookupis kept for the connection-cascade query path.Backward compatibility
subscriptionIdis optional in the bindings schema. MCPs running an older runtime ignore the field. Studio versions that haven't been updated yet keep working — they fall through to the__defaultslot in the new runtime, which preserves the single-sub-per-connection semantics they expect.Test plan
bun testinpackages/runtime— 15/15 pass, including new tests for sibling-disable, multi-sub fanout, and legacy-default fallbackbun testinapps/meshforsrc/storage src/automations src/tools/automations src/tools/connection— 211 pass, 1 skip, 0 failbun run checkclean inpackages/runtimeandpackages/bindingsbun run checkinapps/meshproduces only one pre-existing error indelete-organization-section.tsx(unrelated to this PR)Follow-up (not in this PR)
The gmail MCP at
decocms/mcpswill adopt the newTriggerStorageinterface and update its webhook handler to fanout vialist(connectionId). Filed separately so this PR can land independently.🤖 Generated with Claude Code
Summary by cubic
Enable multiple event trigger subscriptions per connection and cleanly disable them on connection delete. Fixes token overwrites between automations and stops orphaned MCP trigger state.
New Features
TRIGGER_CONFIGUREaccepts optionalsubscriptionId; runtime keys by(connectionId, subscriptionId)and fans out on notify.trigger_callback_tokens.subscription_idwith a unique index); token APIs updated to use subscription ids.trigger-addpre-allocates the trigger id and reuses it for MCP configure;automation/deleteandconnection/deletedisable MCP subscriptions and remove tokens per subscription.subscriptionIdis missing, the legacy__defaultslot is used.Migration
subscription_id, backfill existing rows, and swap indexes; run app migrations.subscriptionIdis recommended to enable multiple subscriptions.Written for commit 2a4e59c. Summary will update on new commits.