Skip to content

Rebuild tool catalogs by upsert-then-prune so reads never see an emptied catalog - #2125

Open
ian-pascoe wants to merge 1 commit into
UsefulSoftwareCo:mainfrom
ian-pascoe:fix/gap-free-catalog-rebuild
Open

ian-pascoe wants to merge 1 commit into
UsefulSoftwareCo:mainfrom
ian-pascoe:fix/gap-free-catalog-rebuild

Conversation

@ian-pascoe

Copy link
Copy Markdown

Summary

Tool-catalog rebuilds used to delete every tool and definition row for a connection and then re-insert them. D1 has no interactive transactions (interactiveTransactions: false), so each statement commits on its own:

  • a search during a rebuild found zero tools for that connection;
  • a rebuild that was cut off or overlapped another session's rebuild left a partial catalog. In production that was a stamped cloudflare_com catalog with 42 of 3600 tools.

The rebuild is now upsert-then-prune:

  1. upsert definitions, then tools, on the tables' existing unique keys (no migration; created_at now means "first seen");
  2. prune by name diff, reading names only, with deletes batched under D1's parameter limit;
  3. stamp last, so an interrupted rebuild leaves every row intact and stays stale.

Definitions are written before the tools that $ref them and pruned after the tools that stop referencing them. Rows go in calls bounded by serialized size: fumadb runs a multi-statement D1 upsert as one native batch, and Cloudflare's catalog (37 MB) exceeded D1's 32 MiB RPC limit (#2123). The empty-catalog clears and removal cascades are unchanged.

Related: #2122, #2123.

Verification

New catalog-persist.test.ts runs rebuilds on the D1 adapter options, with an adapter proxy that pauses or fails writes and enforces a scaled-down batch payload cap:

  • a read paused right after the rebuild's first tool write still sees the full previous catalog; afterwards, added, removed and updated tools and definitions are exact and the connection is stamped;
  • rebuilding an unchanged catalog issues zero deletes;
  • a rebuild whose inserts fail keeps every row and leaves the connection unstamped;
  • a catalog larger than one batch payload lands in full.

All four fail against the previous implementation: the mid-rebuild read got [], the unchanged rebuild deleted tool and definition, the failed rebuild emptied the catalog, and the oversized catalog kept 1 of 400 tools.

Production (self-hosted Cloudflare deployment, cloudflare_com stale-marked, then 20 searches during the rebuild): the first search saw the previous 42 tools (never 0), and every later search saw 3600. D1 afterwards: stamped, 3600 distinct tools, 6900 definitions, with no exceeded* outcomes and no sync failures in wrangler tail. In 40 back-to-back searches across GitHub, Stripe and Cloudflare, 1 returned session_reset, down from ~50% before.

  • oxfmt --check .
  • oxlint -c .oxlintrc.jsonc . --deny-warnings
  • typecheck (tsgo --noEmit) in core/sdk
  • tests: core/sdk 947; also on the combined stack: plugins/openapi 338, plugins/mcp 311, plugins/graphql 118

Checklist

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

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