Skip to content

v6.1.0-edge.2: Workflows get a front door, realtime voice reaches every provider

The third Edge build of the 6.1 line. Workflows become something you can create rather than only inherit: a Workflows app with a real create path, and Flow agents now executing on the durable task-graph dispatcher instead of walking their own graph inside an agent run. Realtime voice stops being OpenAI-shaped — an authored voice now reaches whichever provider resolves. Underneath both, a set of correctness fixes in the caching and data layers, one of which you should take urgently if you run MJAPI.

Edge builds are prereleases. They publish under the edge dist-tag and never move latest.

  • A Workflows app, and a Create Workflow front door inside it. Workflows previously existed only as Flow agents you already had; there was no place to start one.
  • Flow agents run on the durable task-graph dispatcher. FlowAgentType.DetermineInitialStep compiles an agent’s steps and paths into a TaskGraphSpec and returns a Tasks step; BaseAgent.executeTasksStep submits it and detaches. A workflow’s execution is durable, reclaimable after a restart, and observable as a graph rather than as opaque progress inside a run.
  • ModelConfiguration — a per-modality typed configuration bag on the AI model catalog, at three levels forming an inherit-with-override cascade (AIModelType < AIModel < AIModelVendor), resolved base-first with per-key deep merge. One shared interface (IAIModelConfiguration) means CodeGen emits typed accessors on all three entities, so new capability knobs land as typed properties in one bag instead of a column per knob. First consumer is realtime turn detection: an unsupported mode falls back to the profile default rather than rejecting the session, so a shared catalog stays safe across providers.
  • Realtime voice is provider-agnostic. Both override builders filed the authored voice under a hardcoded openai key, so a session resolving to ElevenLabs, Gemini, Inworld, AssemblyAI or HuggingFace silently got nothing — the setting stayed visible in the effective config and never reached a driver. The new realtime.voice.default.voice slot files onto whichever driver resolves, and the mint log now names the resolved driver and the voice that reached it.
  • Realtime voice agents can speak first. An ElevenLabs realtime agent with no first_message produces no audio at all until it hears the user, whatever the persona prompt says — every session opened in silence waiting for the human to guess they should talk (#3557).
  • Speech-to-text via Groq Whisper. GroqAudioGenerator is the first BaseAudioGenerator subclass in MJ that actually implements SpeechToText; the OpenAI and ElevenLabs generators both throw for it. Uses the groq-sdk already depended on. Groq exposes no TTS, so CreateSpeech throws and GetSupportedMethods reports only what works.
  • A Skills button in the composer, beside Plan Mode. / skill commands already worked but were reachable only by knowing to type /, so nothing on screen said the feature existed.
  • The entity-action substrate finishes what its schema promised — seven pieces that shared one failure shape: a column, flag or field that read as configured and did nothing.
  • IsMemberOverridden(instance, member, BaseClassRef) on ClassUtils, which distinguishes “the author made no choice” from “the author chose the value that happens to be the default”.
  • MJAPI cold boot is roughly three times faster. buildSchemaSync was super-linear in entity count from two O(N²) hotspots inside type-graphql. Measured at 1,380 entities: buildSchemaSync 66.3s → 1.2s (54×), total cold boot 95.6s → 32.1s (−66%). Applied via patch-package as pure memoization that preserves schema output exactly; the win grows with entity count and does not affect per-request serving.
  • BREAKING — UserCache moved packages. It now lives in @memberjunction/generic-database-provider and is no longer exported from @memberjunction/sqlserver-dataprovider. There is deliberately no re-export shim: update the import, not just the call.
  • BEHAVIOR CHANGE — RunView 'simple' rows are normalized. Date and numeric columns now hold real Dates and numbers on every tier, matching what the generated entity types declare. Previously the shape depended on where the code ran — three shapes for the same call. Code that read simple-row dates as strings (localeCompare sorts, .slice(0, 10), ISO equality) must now use Date semantics. NULL stays NULL, unparseable values are left alone, and integers beyond Number.MAX_SAFE_INTEGER stay strings so deliberately-string BIGINTs are never precision-corrupted.
  • The entity-search per-entity timeout is configurable, and its default drops from 30s to 3s — a pathological entity is dropped promptly instead of stalling an interactive fan-out. Deployments doing large unindexed scans can raise it.
  • The deterministic suite’s ordering invariant is enforced by a test rather than a convention — every server-transport bundle must sequence before every client-transport one (#3251).
  • PostgreSQL counterparts for this release’s migrations, verified by a clean mj migrate and mj sync push against a database built from nothing.
  • Process-wide server cache corruption — take this urgently if you run MJAPI. ResolverBase mapped GraphQL transport fields onto cached rows, mutating entries every consumer shares. The cache is now structurally unable to be corrupted by consumers (freeze-on-write).
  • ValidateAsync overrides were silently skipped on save. The method documents itself as automatically called by Save() after synchronous Validate() passes. It was not — Save() reached it only when a subclass also overrode something else.
  • Keyset (AfterKey) pagination returned duplicate rows and silently skipped others. Page 1 had no cursor, so it fell through to a row-limited path emitting SELECT TOP N with no ORDER BY at all; page 2 onward forced ORDER BY <pk>. The two orderings disagreed, so page-1 rows reappeared and others were never visited.
  • RelatedRecordCollection.Load() silently discarded unsaved children. Add() and Create() deliberately leave a collection unloaded, so the “already loaded” early return did not protect it: a later lazy read or sibling refresh replaced items wholesale, and the save wrote fewer children than the user entered. It now throws when dirty, naming what would have been lost; Load(true) still discards.
  • GraphQL variables logging failed open. The redactor binds an input type to an entity by name, which works for CodeGen inputs and not for hand-written ones — CreateConnectionInput captures Connection, no entity, so no encrypted fields were contributed and values fell through untruncated. The boot audit tested only the name pattern, so it stayed silent about exactly these.
  • Error diagnostics carried credentials into the log. graphql-request’s ClientError serialises the originating request — variables included — into its own message at construction, and V8 embeds that in stack, so a secret sat on the error in three places at once. SanitizeGraphQLError now builds a diagnostic from an allowlist, keeping status, GraphQL errors, query text and stack frames while logging the variables’ shape rather than values.
  • Realtime token usage was silently discarded for xAI Grok Voice sessions — the two OpenAI-compatible providers put the response.done usage payload in different places.
  • An authored voice never reached a Gemini Live session (#3721). buildConnectConfig spread the config bag onto a LiveConnectConfig, which has no voice property, through an as cast that hid the mismatch from tsc; the converter is a path allowlist, so the value was dropped with no error. The voice is now token-locked on client-direct, making the server’s choice authoritative.
  • A dashboard’s owner could not save it when the dashboard engine had not loaded. Permission resolved solely through the engine’s cache, and a dashboard it cannot find returns “no permissions” — indistinguishable from a real denial. In a CLI process, where engines defer to first use, that refused every dashboard save including the owner’s own.
  • A stopped task-graph dispatcher kept working. pollOnce read running only when the tick fired, then awaited a provider, a rollup and a claim query — so a Stop() landing in any of those gaps left it rolling up graphs and claiming tasks it would never run, which then sat claimed until their lease expired. A host can now also run without the dispatcher entirely (MJ_DISABLE_TASK_GRAPH_DISPATCHER=1).
  • CodeGen could not prune entity metadata on PostgreSQL. Every routine was invoked as SELECT * FROM routine(...), which PostgreSQL rejects for a function returning SETOF record. The pruning pass threw per entity, logged Error removing metadata for entity undefined, and carried on — leaving orphaned EntityField rows that broke mj sync push and the next CodeGen run, with nothing pointing back at the call shape.
  • Open App registration and migrations failed under pnpm (#3677). pnpm’s strict layout resolves bare specifiers from the importing package, which cannot declare runtime-known names; server-bootstrap now resolves runtime-configured packages from the host.
  • Smart Field Identification could leave an entity with zero name fields, overriding a correct deterministic answer and nominating nothing in its place.
  • Auto-created schema “bucket” Applications are hidden from new users. The INSERT omitted DefaultForNewUser, so the DB default of 1 won and every bucket app appeared for everyone.