Skip to content

v5.51.2: Cache integrity fixes for agents, scheduled jobs, and the GraphQL server

A correctness patch for the certified 5.x line. There are no new features, no schema changes and no migrations; all 298 packages move to 5.51.2 in lockstep. Three of the four fixes are in cache-maintenance paths that only misbehave once more than one server is running, which is why they surfaced in multi-instance deployments first. Every change was reviewed and merged on the development line before being backported here.

  • Agent runs and scheduled-job dispatch no longer crash with __mj_CreatedAt?.getTime is not a function (@memberjunction/core, @memberjunction/global, @memberjunction/core-entities, @memberjunction/aiengine, @memberjunction/ai-agents, @memberjunction/scheduling-engine, @memberjunction/scheduling-engine-base): the root cause was BaseEngine.OnExternalCacheChange. When a cross-server cache-change event carried a payload, its rows — plain JSON objects, since cache payloads are serialized — were assigned straight into the engine property, silently replacing BaseEntity instances with plain objects, so a field declared Date held a raw ISO string. Rows are now materialized through TransformSimpleObjectToEntityObject before assignment, the same conversion RunView’s own cache-hit path uses, with 'simple' configs passing through untouched and any failure degrading to the pre-existing full reload. This affected every engine with CacheLocal: true.

  • Date reads across four engines are now guarded (@memberjunction/global): optional chaining does not protect these calls — "…"?.getTime is undefined, and calling it throws. A new exported ToEpochMs() helper backs every affected read in AgentContextInjector, AIEngine, ConversationEngine and the scheduling engine. It also closes a latent defect in the previous form: an invalid Date’s getTime() returns NaN, which ?? 0 did not catch, yielding an incoherent comparator. AIEngine.fallbackGetNotesFromCache is reached whenever the note vector service is uninitialized or a query embedding fails, so semantic retrieval with real input text was exposed too, not only the empty-input path.

  • Scheduled jobs respect their activation windows again (@memberjunction/scheduling-engine): isJobDue compared evalTime < job.StartAt directly. Relational operators coerce toward numbers, so an ISO string yields NaN and every comparison is false — StartAt/EndAt windows silently stopped being enforced and a job could fire outside its range with nothing in the logs. Separately, a poisoned cache made isJobDue throw on the first job in the dispatch loop, stopping all scheduled jobs on every poll until the cache reloaded.

  • Disabled and paused jobs can no longer reach the dispatch loop (@memberjunction/scheduling-engine-base): the engine loads MJ: Scheduled Jobs unfiltered and applies its Active-only invariant in memory, but did not re-apply it after a cross-server cache event, whose payload carries every row. In a multi-instance deployment, one server’s engine load could hand another server’s dispatch loop Disabled, Paused or Pending jobs. The filter is now re-applied after the event, and isJobDue independently refuses non-Active jobs so dispatch never depends on the array staying pre-filtered.

  • Engine caches no longer accumulate duplicate rows for UUID primary keys (@memberjunction/core): BaseEngine.findEntityIndexByPrimaryKeys matched primary-key values with a raw ===, so a UUID arriving in different casing from different sources — a client-minted lowercase id from BaseEntity.NewRecord versus an uppercase value loaded from SQL Server — failed to match, and the event-driven “not found → add it” branch appended a duplicate row. The database stayed correct; every consumer showed the row twice. Matching is now driven off metadata (EntityFieldInfo.IsUniqueIdentifierUUIDsEqual), so there is no string-shape heuristic and non-UUID keys keep exact equality.

  • Preparing a GraphQL response no longer corrupts the server’s own cache (@memberjunction/server): ResolverBase renamed __mj_* keys to their _mj__* wire aliases by writing onto its argument — routinely a row straight out of findBy/RunView, which is the cache’s own object held by reference under a reference-sharing storage provider. Because that cache is process-wide, preparing one response left every later read across every worker serving transport-shaped rows that BaseEntity.SetMany rejects. It reached both the single-record path (UserByEmail, UserByID, UserByEmployeeID, and every generated single-record resolver whose entity has caching enabled) and the RunView result path. Both now map onto copies. Behaviour for callers is unchanged — the same transport-shaped result comes back; what changes is that the provider’s own row objects are no longer written to.

  • Chat-embedded interactive forms no longer clip long text (@memberjunction/ng-forms): agent-authored labels are routinely sentence-length, and the form sized every option to its content width with no ability to shrink, so anything wider than the card was chopped by the card’s overflow: hidden — no ellipsis, no scrollbar, just missing words. Radio and checkbox options now wrap; button groups wrap to a second row instead of hiding options behind an overlay scrollbar in a vertically-scrolling chat column; single-line controls degrade to an ellipsis rather than a mid-word cut; the submitted-answer pill wraps; and the multi-field answer card no longer overflows a narrow message column, where its min-width: 400px unconditionally beat its own max-width: min(800px, 100%). Every rule is inert unless the element was already overflowing.