Repository navigation
fix(adapters,plugins,edge): a restarted server serves its cached bundle in place of an older or refused pull, and doesn't write it back - #1137
Conversation
… an older or refused pull, and doesn't write it back A start has no served bundle, so the forward-only version gate had nothing to compare a pulled bundle with: an endpoint serving an older bundle than the cached last good one (a stale CDN, a rolled-back origin) took the server back, and a pulled bundle refused for its signature left the isolate serving nothing, although the cache held a good one. A start that served the cached bundle after a failed pull also wrote that bundle back to the cache, a needless file or KV write that renewed a KV expirationTtl. While no bundle serves, the sync service now reads the source's cached bundle (the new optional SkillBundleSource.keptBundle(), which SaasPullSource reads from its file or injected cache) and lets it stand in for the served one: - a forward-only source's pulled bundle older than the cached one is not applied, and the cached bundle serves, unless allowRollback; - a pulled bundle that is not applied leaves the cached bundle serving; - a cached bundle that is refused too lets the pulled one serve when it passes. The cached bundle served is never kept again. lastSync says the pull was refused, with the reason and cacheFallback: true, so /readyz shows why, and a Worker's Cron run fails with "was not applied, so the cached bundle serves".
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
…ing' into fix/skilled-cold-start-cache
…ing' into fix/skilled-cold-start-cache
Performance Test ResultsStatus: ✅ All tests passed Summary
Total: 101 tests across 21 projects 📊 View full report in workflow run Generated at: 2026-11-05T17:44:50.557Z |
…ing' into fix/skilled-cold-start-cache
…ing' into fix/skilled-cold-start-cache
|
@coderabbitai review |
Action not completed
|
…ing' into fix/skilled-cold-start-cache
|
@coderabbitai review |
Action not completed
|
* Cherry-pick: fix(adapters,plugins,edge): an older saas bundle, pulled or pushed, doesn't roll the server back Cherry-picked from #1119 (merged to release/1.11.x) Original commit: d88f422 Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com> * Cherry-pick: fix(sdk,plugins): an @app's Skilled OpenAPI credential resolver reaches execute_action and this.callTool() of an operation Cherry-picked from #1120 (merged to release/1.11.x) Original commit: 00a3b00 Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com> * Cherry-pick: fix(adapters,plugins,edge): a push token replayed to another replica or Worker isolate is refused Cherry-picked from #1122 (merged to release/1.11.x) Original commit: 1341026 Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com> * Cherry-pick: fix(plugins): a skill a skills:filter hook hides can't be run by naming its actions Cherry-picked from #1123 (merged to release/1.11.x) Original commit: a081fbf Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com> * Cherry-pick: fix(sdk): a plugin listed twice in one plugins list is refused at startup instead of keeping only its last entry Cherry-picked from #1127 (merged to release/1.11.x) Original commit: 3dbadd3 Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com> * Cherry-pick: fix(plugins,edge): a Cron run that doesn't apply its bundle fails, and /readyz says the bundle served is stale Cherry-picked from #1128 (merged to release/1.11.x) Original commit: 4938d21 Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com> * Cherry-pick: fix(sdk): connect() with an authToken connects an anonymous caller again on a server without auth Cherry-picked from #1129 (merged to release/1.11.x) Original commit: 3d238da Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com> * Cherry-pick: fix(auth,adapters,plugins,edge): operators can tell why a pull or push token was refused, while callers still get the generic answer Cherry-picked from #1130 (merged to release/1.11.x) Original commit: 913f4f5 Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com> * Cherry-pick: fix(sdk,auth,plugins): this.authProviders works: the SDK wires the AuthProviders vault from a new authProvidersVault option Cherry-picked from #1131 (merged to release/1.11.x) Original commit: e14c30c Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com> * Cherry-pick: fix(sdk,auth): /oauth/connect connects a verified caller who has no vault yet, and asks an anonymous one to sign in Cherry-picked from #1132 (merged to release/1.11.x) Original commit: f46a87a Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com> * Cherry-pick: fix(sdk,plugins): the SDK skill surfaces say a plugin's skills haven't loaded yet, retryable, and carry bundleVersion Cherry-picked from #1133 (merged to release/1.11.x) Original commit: 0d1dc94 Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com> * Cherry-pick: fix(sdk,auth,cli,plugins): the credentials-required error keeps its connect link out of the message, and asks through URL elicitation where the client supports it Cherry-picked from #1134 (merged to release/1.11.x) Original commit: 6d8ea24 Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com> * Cherry-pick: fix(sdk): a health check registered after startup runs in /readyz, and registerHealthProbe returns a remover Cherry-picked from #1135 (merged to release/1.11.x) Original commit: 7f2f48f Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com> * Cherry-pick: fix(sdk,plugins): a bundle swap sends one list_changed per list it changed, not one per entry Cherry-picked from #1136 (merged to release/1.11.x) Original commit: 0296291 Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com> * Cherry-pick: fix(adapters,plugins,edge): a restarted server serves its cached bundle in place of an older or refused pull, and doesn't write it back Cherry-picked from #1137 (merged to release/1.11.x) Original commit: 358d114 Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com> * Cherry-pick: fix(sdk,adapters,plugins,edge): a push token with a body_sha256 claim can't carry another bundle Cherry-picked from #1138 (merged to release/1.11.x) Original commit: b870325 Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com> * Cherry-pick: fix(plugins,edge): managed mode takes confirm, and the audit log says whether the user confirmed a call Cherry-picked from #1139 (merged to release/1.11.x) Original commit: 588289f Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com> * Cherry-pick: fix(cli): the generated CLI says unknown command, prints an MCP error prefix once, and offers load_skill only the run lines it has Cherry-picked from #1140 (merged to release/1.11.x) Original commit: 4cfe9a2 Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com> * Cherry-pick: fix(plugins): load_skill's outputSchema describes a loaded skill once Cherry-picked from #1141 (merged to release/1.11.x) Original commit: 470010e Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com> * Cherry-pick: fix(sdk,cli): skills/load's default answer sends formattedContent again, as 1.11.0 did; format: 'markdown' is the opt-in for the smaller one Cherry-picked from #1142 (merged to release/1.11.x) Original commit: a680fe7 Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com> --------- Co-authored-by: agentfront[bot] <agentfront[bot]@users.noreply.github.com> Co-authored-by: frontegg-david <69419539+frontegg-david@users.noreply.github.com>
Stacks on #1136.
What and why
The
saassource keeps a last-good bundle cache: a file inbundleCacheDiron Node, KV on a Worker. Three gaps showed when a server or isolate starts with nothing served yet:expirationTtl, so a fallback the bundle server never confirmed stayed alive across restarts.The cached bundle stands in for the served one. While no bundle serves, the sync service reads the source's cached bundle and uses it in place of the served bundle the gate has yet to compare with:
keptBundle(), the read side ofkeepBundle().SaasPullSourcereads the file or the injected cache, through the same schema check as before.compareBundleVersions) is not applied. The cached bundle serves instead. WithallowRollback: truethe pulled bundle serves, as before.allowRollbacktoo.409 bundle_outdated.The cached bundle is never written back. A bundle the source hands over with
origin.pullFailure(the cached one, served because the pull failed) is not kept again, and neither is the cached bundle served in place of an older or refused pull.The load state says why. When the cached bundle serves in place of the pulled one,
lastSyncisrefused, with the pulled bundle's id and version, the reason, andcacheFallback: true. For an older pull the reason isolder than the cached bundle <id>@<version> (generatedAt <date>)./readyzshows theskilled-openapicheck asdegraded(still ready), with the cached bundle as the one served. The log says[bundle-sync] bundle <pulled> not applied: <reason>; serving the cached bundle <cached>. On a Worker the Cron run fails withbundle <id>@<version> was not applied, so the cached bundle serves: <reason>.The change sits in the source and the sync service's apply path before
apply().apply()is unchanged. The content-digest skip from #1136 doesn't come into play, since a cold start has no served bundle. An identical cached and pulled bundle is applied once, from the pull, and reportedapplied.Scenarios covered:
saassource's cache, on Node and on a Worker./readyzand in the Cron run.User-facing change
saassource, serves its cached bundle in place of a pulled bundle that is older than it (unlessallowRollback) or that is refused.BundleSyncOutcome'srefusedstatus has an optionalcacheFallback: true, set when the cached bundle serves in place of the refused one./readyzshows it inlastSync.was not applied, so the cached bundle serves: ...in that case.@frontmcp/adapters/skills:SkillBundleSourcehas an optionalkeptBundle(), whichSaasPullSourceimplements.acme:billing@1.4.0, with1.5.0cached: served1.4.0and cached it → serves1.5.0and keeps it cached./readyzshowslastSync: { status: 'refused', bundleVersion: '1.4.0', reason: 'older than the cached bundle acme:billing@1.5.0 (...)', cacheFallback: true }./readyz503 andSKILL_BUNDLE_NOT_LOADEDfrom every tool → the cached bundle serves,/readyzis ready with the checkdegraded./readyz)frontmcp-deployment/references/deploy-to-cloudflare-skills-only.mdfrontmcp-production-readiness/references/health-readiness-endpoints.mdThe plugin's and the edge's READMEs are updated too.
Tests
Added:
@frontmcp/adapters:saas-pull.source.spec.ts:keptBundle()reads back the kept bundle without pulling, and nothing when none is kept or the file fails the schema check.saas-pull.source.edge.spec.ts: it reads the injected cache through the schema check; an offline start reads the cache and writes nothing.bundle-sync.service.spec.ts:allowRollbackserving an older pull;outdated;last-good-bundle.integration.spec.ts(a real server): a restart during an outage writes nothing to the cache; a Cron refresh that starts it against an older bundle serves the cached one and resolvesrefusedwithcacheFallback; a restart whose pull fails its signature serves the cached one.index-wiring.spec.ts: the Cron run's error for a refused pull with the cache serving.index.spec.ts(a real scope): an isolate started against an older bundle fails its run, serves the cached one, and/readyzsays why; no isolate start writes the cache.demo-e2e-skilled-openapi, saas on Node:/readyzsays why.demo-e2e-cloudflare, managed Worker in workerd:/readyznames the refusal;Run:
npx nx run-many -t build -p sdk,adapters,plugin-skilled-openapi,edge,testing: passed.@frontmcp/adapters: 63 suites, 1128 tests, passed.@frontmcp/plugin-skilled-openapi: 53 suites, 822 tests, passed.@frontmcp/edge: 10 suites, 110 tests, passed.@frontmcp/skills: 3 suites, 249 tests, passed.npx nx test demo-e2e-skilled-openapi: 11 suites, 97 tests, passed.npx nx test demo-e2e-cloudflare: 3 suites, 32 tests, passed.npx nx run <project>:typecheckfor adapters, plugin-skilled-openapi, edge and demo-e2e-skilled-openapi, andnpx tsc --noEmit -p <lib>/tsconfig.lib.jsonfor adapters, plugin-skilled-openapi and edge: no errors.The SDK is unchanged, so the bundle-size e2e doesn't apply.
Summary by CodeRabbit