Skip to content

Narrow plugin storage prefix reads in SQL and bound stale catalog rebuild concurrency - #2124

Open
ian-pascoe wants to merge 2 commits into
UsefulSoftwareCo:mainfrom
ian-pascoe:fix/scoped-operation-scan
Open

ian-pascoe wants to merge 2 commits into
UsefulSoftwareCo:mainfrom
ian-pascoe:fix/scoped-operation-scan

Conversation

@ian-pascoe

Copy link
Copy Markdown

Summary

On the Cloudflare host, every tool search could run the MCP session Durable Object out of memory once a large OpenAPI spec (Cloudflare's own API, 3600 operations) was connected. The OpenAPI store listed a connection's operations by loading and decoding every OpenAPI integration's rows and then filtering in JS, and each stale rebuild did that again.

  • pluginStorage.list and collection query now push keyPrefix into SQL (starts with) and keep the exact JS startsWith. SQL only narrows the read, so results are unchanged for every caller. Prefixes containing \ stay JS-only (Postgres LIKE escape; see fumadb drizzle: 'starts with' is an unescaped LIKE, and D1 batch upserts ignore the 32 MiB RPC cap #2123).
  • The OpenAPI store lists by its integration-scoped key prefixes (op.<hash>. and the legacy <integration>.), decodes each row once, and keeps an integration === guard. GraphQL's store already passes keyPrefix and gets the same narrowing.
  • New ExecutorConfig.toolsSyncConcurrency (default 10, unchanged), forwarded through HostConfig. The Cloudflare host sets 2: each in-flight rebuild holds its resolved catalog until its write commits, and writes are serialized anyway.

Related: #2122 (no backoff for failing syncs), #2123 (fumadb starts with).

Verification

Heap, measured locally on the D1 adapter options (interactiveTransactions: false, maxBoundParameters: 100) with real co-tenant specs (Cloudflare, GitHub, Stripe ×2, Sentry, Vercel; 5,123 operation rows):

Scenario Before (live / raw) After (live / raw)
Single Cloudflare resync 40.8 / 59.5 MB 21.3 / 39.7 MB
All 6 connections stale, 3 ms/statement 66.1 / 108.4 MB 33.6 / 61.7 MB (concurrency 2)

The operation read went from all 5,123 rows to the connection's own 2,726. Concurrency 2 and concurrency 1 had the same wall time (21.7 s vs 23.3 s, within noise).

Production (self-hosted Cloudflare deployment): before, wrangler tail showed McpSessionDO exceededMemory on searches, and ~50% of searches across all integrations failed with -32010 session_reset. With this deployed there were no exceededMemory outcomes. The Cloudflare catalog still did not rebuild fully because of the delete-then-insert rebuild on D1, which the companion PR fixes.

  • oxfmt --check .
  • oxlint -c .oxlintrc.jsonc . --deny-warnings
  • typecheck (tsgo --noEmit) in core/sdk, core/api, host-cloudflare, plugins/openapi, plugins/mcp
  • tests: core/sdk 944, plugins/openapi 338, plugins/graphql 118, plugins/mcp catalog-sync 9
  • New tests fail without the change: the storage read returned 50 rows instead of 2, and the OpenAPI store listed with no prefix.

Checklist

  • Added a changeset.
  • Added or updated tests for the new behaviour.
  • No secrets, credentials, or private data in the diff.

@ian-pascoe ian-pascoe closed this Sep 25, 2026
@ian-pascoe ian-pascoe reopened this Sep 25, 2026
@ian-pascoe ian-pascoe closed this Sep 25, 2026
@ian-pascoe ian-pascoe reopened this Sep 25, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant