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.
Bug Fixes
Section titled “Bug Fixes”-
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 wasBaseEngine.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 replacingBaseEntityinstances with plain objects, so a field declaredDateheld a raw ISO string. Rows are now materialized throughTransformSimpleObjectToEntityObjectbefore 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 withCacheLocal: true. -
Date reads across four engines are now guarded (
@memberjunction/global): optional chaining does not protect these calls —"…"?.getTimeisundefined, and calling it throws. A new exportedToEpochMs()helper backs every affected read inAgentContextInjector,AIEngine,ConversationEngineand the scheduling engine. It also closes a latent defect in the previous form: an invalidDate’sgetTime()returnsNaN, which?? 0did not catch, yielding an incoherent comparator.AIEngine.fallbackGetNotesFromCacheis 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):isJobDuecomparedevalTime < job.StartAtdirectly. Relational operators coerce toward numbers, so an ISO string yieldsNaNand every comparison is false —StartAt/EndAtwindows silently stopped being enforced and a job could fire outside its range with nothing in the logs. Separately, a poisoned cache madeisJobDuethrow 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 loadsMJ: Scheduled Jobsunfiltered 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, andisJobDueindependently 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.findEntityIndexByPrimaryKeysmatched primary-key values with a raw===, so a UUID arriving in different casing from different sources — a client-minted lowercase id fromBaseEntity.NewRecordversus 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.IsUniqueIdentifier→UUIDsEqual), 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):ResolverBaserenamed__mj_*keys to their_mj__*wire aliases by writing onto its argument — routinely a row straight out offindBy/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 thatBaseEntity.SetManyrejects. It reached both the single-record path (UserByEmail,UserByID,UserByEmployeeID, and every generated single-record resolver whose entity has caching enabled) and theRunViewresult 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’soverflow: 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 itsmin-width: 400pxunconditionally beat its ownmax-width: min(800px, 100%). Every rule is inert unless the element was already overflowing.