Documentation / Exploitation / Guides pratiques / Upgrade the platform

Upgrade the platform

Meridian has two independently versioned things to upgrade: the platform (the four core libraries baked into the meridian-api and meridian-config-plane images) and plugins (published separately and resolved by the running host at boot). This recipe covers both, and — this is the part that bites if done out of order — the sequencing between them.

Before starting, make sure you already have a registry set up (Set up a plugin registry) and know how your instance declares its plugins (Declare and install plugins).

Before you start

  • Images never bundle business plugins. Dockerfile installs only the workspace and the core plugins (external-plugins/, copied into the image); every plugin under plugins: in the instance manifest is resolved against the registry (or a path override) at boot, in ensureInstancePlugins (apps/api/src/registry.ts). A plugin that is listed but cannot be resolved — no registry configured, version not found, download failed — fails the boot loudly, the same as an invalid manifest. Practically: publish a plugin version before you deploy or restart anything that will require it. Doing it the other way round turns a routine restart into an outage.

  • Version resolution is real semver, via the npm semver package, and it treats the two kinds of plugins differently. Keep them apart when you plan a rollout:

    • A registry plugin (plugins: with a version range) is resolved at boot (resolveRequiredPlugins, libs/plugin-cli/src/registry/resolve.ts): the host picks the highest combination of published versions of the instance ranges whose sdk: range admits its plugin contract and whose dependsOn ranges agree with the plugins it loads and with each other. A version that does not fit is set aside for the next one, with the warning « plugin requis : versions plus hautes écartées ». When no combination fits, or the range has no published version, it is a hard failure: the boot aborts.
    • A plugin found on PLUGINS_PATH or through a path: entry is not resolved, even when plugins: lists it: the local copy loads, and a version outside the instance range only logs a warning. The host checks its sdk: range against its plugin contract and its dependsOn ranges against the versions of its dependencies when it loads it (apps/api/src/plugins.ts). A mismatch is a soft failure: the plugin is skipped with the log line contrat plugin incompatible — plugin ignoré or « dépendance signalée », and the rest of the host still boots.

    A plugin that uses declared events under a range below ^0.1.1 is a third case: it loads, with the warning « plage sdk insuffisante ».

  • A run's workflow spec is journaled when the run starts, and so is the result of each step it has executed. Workflow YAML changes never affect a run already in flight. Plugin code can, depending on the journal format the run started with. When a run resumes after a restart, the nodes it has not executed yet run the new code. A run of format 2 also reads from its journal its trigger's outputs and the texts and outputs of the human tasks it has reached: a new plugin version that changes the declared event, the trigger's outputs or a task's title applies to new runs only. A run of format 1 computes them again with the loaded plugins: when the new version gives the run's event other outputs (a field added to or removed from the declared event, an output or its from changed, a coded fromEvent changed) or a task another title, the replay of a run past that point, such as a suspended or waiting run, no longer matches the journal, and Restate retries the run until the previous version is loaded again. See Why durable execution and Recover runs blocked by a missing plugin.

1. Publish new plugin versions first

Do this regardless of whether the platform itself is also changing — it's the step that must land before anything else touches the affected environment.

  1. From the plugin's own repo, build the deliverable dist/ (esbuild bundles every module the manifest references, inlining its runtime dependencies; only node:* and @meridian/* stay external):

    meridian-plugin build .
    
  2. Publish it to the target registry (local directory or gs:// bucket):

    meridian-plugin publish . --registry gs://meridian-plugins
    # or: export MERIDIAN_REGISTRY=gs://meridian-plugins && meridian-plugin publish .
    

    A published version is immutable — publishPlugin refuses to overwrite an existing @publisher/name/<version> (libs/plugin-cli/src/registry/registry.ts). If you need to ship a fix, bump the version in plugin.yaml and publish again; there is no in-place replace.

  3. Confirm it landed before moving on:

    meridian-plugin versions @posos/demo --registry gs://meridian-plugins
    

See Build and bundle and Publish to the registry for the authoring side of this in full, and Bundling and distribution for why the artifact is self-contained.

2. Point instances at the new plugin version

This is safe to do any time after step 1 — the registry now has what any instance will ask for.

  • If the instance manifest's range already covers the new version (e.g. "@posos/demo": "^0.1.0" and you published 0.1.2), no manifest change is needed: the next endpoint/proxy restart resolves the new highest match automatically, once its dependsOn accepts the plugins the instance loads (see Business plugins that require a newer core plugin).
  • If you need the new version specifically, tighten or bump the range in the manifest and restart endpoint + proxy so ensureInstancePlugins re-resolves it. See Declare and install plugins and the instance manifest reference for the plugins: block.
  • A stale local cache never blocks an upgrade to a newer version — only the requested version is looked up, downloaded once into MERIDIAN_PLUGIN_CACHE, and reused afterwards.

3. Upgrade the platform libraries (lockstep)

Do this when @meridian/shared, engine-core, llm-client, or plugin-cli change — they version together as a fixed group (.changeset/config.json, fixed), so plugin authors only ever have to check one number.

  1. Any PR touching one of the four libraries includes a changeset:

    pnpm changeset
    
  2. On merge to main, the release workflow (changesets/action, .github/workflows/release.yml) opens or updates a "Version PR". Merging that PR runs:

    pnpm run version-packages   # changeset version && lockfile refresh
    

    which bumps all four package.json versions identically. Note that PLUGIN_CONTRACT_VERSION (libs/shared/src/version.ts) — the constant the host compares every plugin's sdk: range against — is decoupled from the package version and changes with the plugin contract: its patch for a new manifest key an older host would strip (0.1.1, declared events; 0.1.2, resolve.pending on an event binding; 0.1.3, the default filter of a declared event; 0.1.4, the description of a protocol's reception; 0.1.5, conditional port rules; 0.1.6, connections), its minor on a real break of the plugin-facing surface. A plugin that declares sdk: "^0.1.6" is refused by a host older than that contract, so deploy the host before such a plugin (@posos/fhir 0.1.7 and @posos/posos 0.6.4 declare ^0.1.6, @posos/hl7 0.2.1 and @posos/pn13 0.1.1 ^0.1.4, @posos/common 0.4.1 ^0.1.3). Publishing these packages to an external npm registry is deliberately disabled for now (release.yml): they're consumed in-workspace, so "upgrading the platform" means building new images from the merged main, not installing a new SDK release.

  3. Build the images from that commit. endpoint and proxy are the same meridian-api image, started with different commands (pnpm --filter @meridian/api run restate vs run server — see Dockerfile); ui is the separate meridian-config-plane image (Dockerfile.ui). For a compose deployment:

    pnpm run build:plugins   # external plugins mounted via PLUGINS_PATH, not baked in
    docker compose --profile stack up -d --build
    

For the full changeset mechanics (drafting a changeset, reviewing the Version PR), see Release and version.

4. Roll out in order

Once the images (if any) are built and the plugin versions you need are published, restart in this order — it's the order that keeps in-flight runs safe and keeps every component talking to a component it can still reach:

  1. Endpoint — restart it first. It re-registers with Restate on boot; any run already in progress resumes where the journal left it, which is the durability guarantee Restate gives you (no drain step needed), provided the new release replays the run's journal format: the same commands, and state writes of the same shapes. A release changes neither within a format; a new format is started only once RUN_JOURNAL_FORMAT is set to it. See Why durable execution.
  2. Proxy — restart next.
  3. UI — restart last; it is stateless and only serves the console.
  4. Restate itself upgrades on its own schedule and procedure, independent of the four services above — keep the restate-data volume across the upgrade so the journal survives.

Test the exact platform + plugin version combination in pre-production first; the same advice applies to plugin swaps and to a platform bump alike.

Verify Restate service discovery

The endpoint registers through RESTATE_ENDPOINT_URL, often a Kubernetes Service. During a rolling update that address can still select an older pod before the new one becomes Ready. A successful HTTP response from the admin API is only accepted when it lists every service bound by the new endpoint. An incomplete response logs enregistrement incomplet with the missing names and retries every three seconds within the registration budget. endpoint Restate démarré logs the expected names; déploiement enregistré logs the discovered names.

If authoring returns 404 service 'meridianWorkflowAssist' not found, compare GET /services on Restate's admin API with that list. Both meridianWorkflowAssist and meridianWorkflowTests must be registered. Once the new endpoint is Ready, registering its existing address again through POST /deployments with { "uri": "<RESTATE_ENDPOINT_URL>", "force": true } refreshes discovery. Retry the generation after registration succeeds.

Workflows that use a new core node

A platform version can add a core node type. An endpoint older than that version does not know the type, and the proxy validates with its own catalogue, so it does not report the mismatch. The core node types added so far:

Node type On an endpoint that does not know it
flow.merge (merge branches) When a wired input of an unknown node is absent, the endpoint skips the node (node-skipped-unknown-type) instead of failing the run. A merge of exclusive branches always has an absent input, so an older endpoint skips it together with everything downstream, without an error.
trigger.event-received (« Événement reçu », the trigger configured by a declared event) A new run fails at its trigger (Trigger invalide : trigger.event-received). A run in flight throws the same way at its next replay, before its first journaled entry: Restate reports a journal mismatch and retries the invocation until the endpoint knows the trigger again, or until the invocation is killed. The run keeps its last state meanwhile; the endpoint logs rejeu interrompu : … once at warn and does not count it as failed (Run outcome).
  • Publish no workflow that contains one of these node types until every endpoint replica runs an image that knows it.
  • Before rolling the endpoint back to an image older than such a node type:
    1. Unpublish the workflows that contain it (Unpublish in the workflow's version panel, or POST /api/workflows/<slug>/unpublish). This stops new runs only.
    2. Wait until no run of these workflows is running, suspended or waiting in the Executions view, or cancel those runs in Restate while the endpoint still runs the current image. An image that does not know the node cannot replay the journal of a run that already executed it: the run stops on a Restate journal mismatch, or, for flow.merge, fails with DeploymentError when every wired input of the merge is present. A run that has not reached a merge yet would skip it and everything downstream.
    3. Roll the endpoint back.

A run blocked on a journal mismatch after a rollback or a plugin removal is detected, restored or ended as described in Recover runs blocked by a missing plugin.

Journal formats

The journal format of a run fixes the commands its replay must reproduce (the durable steps, their names and order) and the values it writes: the shapes of the set state values of the snapshot, the trace and the observations. Restate compares both at each replay. The proxy starts each run in the format that RUN_JOURNAL_FORMAT chooses (1 or 2, 1 by default; see Environment variables), logs it at boot (format de journal des runs démarrés : <n>), passes it in the invocation argument (journalFormat) and records it in the run index (journal_format, shown by the run detail and the runs list). The endpoint runs and replays each run in that format, whatever its own release. The formats so far:

Format Runs of this format A replay reads from the journal
1 Started with RUN_JOURNAL_FORMAT=1 (the default), or by a proxy older than format 2 (no journalFormat in the invocation argument, journal_format NULL). The spec, the event and the result of each step. The trigger's outputs and the title, payload, deadline and outputs of each human task are computed again by the loaded plugins, and no step closes a validation request.
2 Started with RUN_JOURNAL_FORMAT=2. Also the trigger's outputs (step trigger), the request of each human task (step <node>/request) and its outputs (step <node>/outputs). Each validation request is published and closed in a step (publish-validation-request, close-validation-request), whatever the record ports bound.

A run of format 2 still takes from the loaded plugins, and can still diverge on: a node type's ports, configuration and output types, the removal of a node type, and the checks of the outputs of the other nodes, which run again at replay against the loaded ports. A task pending when a new plugin version loads gets that version's onResolve or onTimeout.

  • Rollout. Deploy the release on every endpoint replica, then set RUN_JOURNAL_FORMAT=2 on the proxy. The api image carries both processes and a deployment usually bumps them together, so the variable, not the image order, keeps runs of format 2 away from an endpoint that does not know it. An endpoint older than format 2 ignores journalFormat: it runs a run of format 2 with the commands of format 1, and once the endpoint is upgraded that run no longer matches its journal and stays blocked. An endpoint that knows format 2 but not a later format fails a new run of that format at once: « Format de journal 3 inconnu de ce moteur (formats connus : 1, 2). Déployer l'endpoint dans une version qui le connaît. »
  • Rollback. An endpoint that does not know a format cannot replay its runs: they stop on a journal mismatch (rejeu interrompu : …, see Run outcome) until an endpoint that knows it runs again. To roll the endpoint back below format 2:
    1. Set RUN_JOURNAL_FORMAT=1 on the proxy and restart it: new runs start in format 1.

    2. Wait until no run of format 2 is in flight. On the run index (RUN_DB_URL), this query returns no row:

      SELECT id, workflow_file, created_at FROM runs
      WHERE final_status IS NULL AND journal_format = 2;
      

      A run that has ended is frozen by the next settlement pass (see Run index).

    3. Roll the endpoint back.

  • Runs in flight at the upgrade keep their format. A run of format 1 stays exposed to a plugin version that changes its event or the title of a task it waits on, and a console decision does not close its validation request in the record (see Recover runs blocked by a missing plugin).

Business plugins that require a newer core plugin

The core plugins (@posos/common, @posos/clinical, @posos/fhir…) come from the image; the business plugins come from the registry, published when their change merges. A business plugin version whose dependsOn requires a core plugin version that the deployed image does not carry, or whose sdk range requires a plugin contract the image does not have, is not selected at boot: the highest version of the instance range that the image accepts is, and the boot logs « plugin requis : versions plus hautes écartées » with the version set aside and its reason. The required plugins are resolved together, with backtracking: when the newest version of a business plugin requires a version of another required plugin that its instance range excludes, or that the image does not accept, the previous version of the first one is selected, with a version of the other that fits. Once every endpoint and proxy replica runs the image that carries the required version, the next restart selects the new version. A rollback of the image works the same way: the business plugin versions that the previous image does not satisfy are set aside at the next boot.

Every process resolves on its own at boot, with the same registry and the same image: endpoint and proxy select the same versions. During a rollout, a replica of the old image and a replica of the new one can select different versions until the rollout ends.

When no combination of versions of the instance ranges fits, the boot fails. The versions set aside are logged first, then the message names the plugin left without a version and what rules its versions out, three versions at most; a reason that cites another required plugin marks it « (requis) », a version the resolver examined rather than a loaded plugin. Publish a compatible version, deploy the image first, or change an instance range.

A business plugin listed in plugins: that is also found on PLUGINS_PATH is not resolved on the registry: the copy of the image loads, and the boot warns when its version is outside the instance range.

Plugin dependency versions

The host checks each dependsOn range of a plugin against the version of the dependency found (« dépendance signalée », kind dependency), whether that copy loads or is refused itself. A plugin whose range does not accept that version, whose range is invalid, or that cites itself, is not loaded; a plugin whose dependency is not found, or was refused with a version in the range, loads without the contributions that cite it. The repository's plugins declare ^<major>.<minor>.0 of the series they are tested with and load with no warning; a plugin that declares * is not constrained, as before.

For plugin authors, meridian-plugin validate and build fail on the same problems, and on a dependency they cannot find (neither in node_modules nor in a sibling directory). For a dependency above the declared range, they print:

  - @acme/vitals › @posos/clinical (dependency) : « @posos/clinical » en 0.3.0, hors de la plage « ^0.2.0 », plugin non chargé. Charger une version de « @posos/clinical » dans « ^0.2.0 », ou déclarer "^0.3.0" si le plugin fonctionne avec 0.3.0.

meridian-plugin deps lists each dependency with its range, the version found and its state. publish refuses an invalid range, and a dependency published on the target registry whose versions are all outside the range:

  - @acme/vitals › @posos/demo (dependency) : toutes les versions de « @posos/demo » publiées sur gs://meridian-plugins sont hors de la plage « ^0.3.0 ». Publier une version de « @posos/demo » dans « ^0.3.0 » d'abord ou corriger la plage.

Review the plugin against the new version of the dependency before moving its range to the new series; a new range is a new plugin.yaml, so it needs a new plugin version (see Release and version).

Platform metrics under meridian.*

The release that names the platform's metrics under meridian. exports no clinical.* metric: clinical.events.ingested becomes meridian.events.ingested, and so on for the fourteen instruments, with the same attributes, units and kinds (Observability signals). The OTel log scope of the OTLP export becomes meridian instead of clinical. Change the dashboards, alerts and queries that read the old names in the same deployment as the proxy and the endpoint: there is no period during which both names are exported.

Plugin manifests under contract 0.1.3

A host on contract 0.1.3 refuses two manifest forms that older hosts loaded, and reports each at load:

  • a trigger without pinnedRouting and without event (an eventType alone): the trigger is dropped (« déclencheur signalé »). Declare the event in contributes.events and reference it with event;
  • defaultSelector on a node: the node loads, the key is ignored (« déclencheur signalé » on a trigger, « nœud signalé » on another node). On a trigger with event, move the filter to that event (events[].defaultSelector) and declare sdk: "^0.1.3"; elsewhere, remove it.

A declared event whose type is the address of a pinned trigger is refused as well (« événement signalé » for the plugin that declares it, « déclencheur signalé » for the plugin that pins the address): rename the event or change the address. A declared event with a field named done is refused too.

The host also drops, and reports, two forms that older hosts loaded:

  • an agent without model (or with an empty provider, no input or no output): the agent is dropped alone, the rest of the plugin loads (« nœud signalé », agent « X » : modèle absent, agent ignoré. Renseigner « model ».). Fill in the model;
  • a declared event field or a node port on a dimension from outside the plugin's dependsOn closure: the event or the node is dropped (« événement signalé » or « nœud signalé », dimension « mass » inconnue dans la fermeture dependsOn. …). The core declares no dimension: the ten common physical dimensions (mass, volume, time, the concentrations…) come from @posos/common 0.4.1, so a plugin that types a port on one of them declares @posos/common in dependsOn.

meridian-plugin validate and build report these problems as the host does on the same manifests (of the host's own node checks, only the dimensions of the ports are run by the CLI; an unknown type on a port is not), so fix a plugin before publishing it for such a host; build reports an address conflict to the plugin it builds when the other plugin is in its dependsOn closure.

Unknown manifest keys

The manifest schema drops a key it does not know. The host reports each such key at load, named by its path (« clé ignorée », kind key), and loads the plugin without it, as earlier hosts did silently: a plugin already published loads the same. The plugin contract does not change (0.1.3).

For plugin authors, meridian-plugin validate and build fail on an unknown key, and publish refuses a dist/plugin.yaml that carries one, or that the schema of the CLI's plugin contract rejects (a dist/ whose sdk range does not accept that contract is published without these checks). A manifest that an earlier CLI built can carry such keys: a misspelled key, the rest of a value cut by an unquoted comma in a { … } mapping, or an agent's input shorthand next to its inputs list. Fix the manifest (quote the value, correct or remove the key) before building a new version:

  - @acme/vitals › contributes.nodes[0].outputs[1]["émis si l'événement le porte."] (key) : clé inconnue, ignorée : la valeur de « description » est coupée à une virgule. Mettre cette valeur entre guillemets.

Protocol reception under contract 0.1.4

The host is on contract 0.1.4: a plugin describes the reception of its protocol in the manifest (media types and an example message of an HTTP connection, the default port and the hint of each of its own transports), and the core names no protocol.

  • Plugins. @posos/hl7 0.2.1 and @posos/pn13 0.1.1 declare sdk: "^0.1.4". A host on contract 0.1.3 refuses them, pn13 with « contrat plugin incompatible — plugin ignoré », hl7 with « manifeste invalide » (its MLLP transport is written as an object, which the earlier schema does not read). They ship in the image with the host, so a normal rollout loads them together. Plugins in ^0.1.x stay accepted. A plugin that lists http or directory among its own transports loads with « déclencheur signalé »: remove the name, the host offers both to every protocol.
  • Console and API. GET /api/connections/protocols serves each transport of a protocol as an object ({ name, defaultPort?, hint? }), with the hint in the language of ?locale=. The console of this release reads both forms; an earlier console does not read objects, so deploy the console with or before the proxy. The routing audit and the hospital simulator read only the bindings of the route. The connection form proposes the transport's declared port (2575 for MLLP) and shows its hint.
  • Validation refusals. An HTTP refusal (422) carries error « Validation refusée (profil « PN13-IS 4.0.34 »). », the profile named by its label; the inbox history shows « Validation refusée. » next to the report. A client that matches the text should read the status code and validation.status.
  • OpenAPI. The request body of an HTTP connection lists the media types and the example of its protocol: application/xml and text/xml for PN13 and CDA R2, application/hl7-v2 and text/plain with an example message for HL7 v2.

Connections under contract 0.1.6

The host is on contract 0.1.6: a plugin contributes connection types and requires a connection on an adapter, an event source or a node, and the host gives it an authenticated handle on the connection the instance designates, or on the default connection of the type. See Manage connections.

  • Plugins. @posos/fhir 0.1.7 (connection type fhir-r4) and @posos/posos 0.6.4 (connection type posos-api) declare sdk: "^0.1.6". A host on contract 0.1.5 refuses them (« contrat plugin incompatible — plugin ignoré »), so upgrade the host first. Plugins in ^0.1.x stay accepted.
  • Instance manifest. Move the FHIR URL (ports.*.config.baseUrl; for sources.fhir-poll.baseUrl, see Event sources as inbound connection types) and the variables FHIR_BASE_URL, POSOS_API_URL, POSOS_AUDIENCE and POSOS_REGION to connections:, and name the connection used by default for each type in connectionDefaults:. The old forms are still read for one version, with a warning ([fhir] config.baseUrl est déprécié …, [posos] POSOS_API_URL, POSOS_AUDIENCE et POSOS_REGION sont dépréciés …).
  • Posos defaults. The Posos plugin no longer falls back to the preproduction URL and audience: the instance supplies them through a connection or the variables above.
  • Credentials. Set MERIDIAN_SECRET_KEYS on both the proxy and the endpoint when a connection created in the console uses a credential. See Manage credentials.

Validation channels under contract 0.1.8

The host is on contract 0.1.8: a human validation can be published to a messaging channel and decided there, a validation can offer a choice between options, and incoming decisions arrive as messages of the validation-decision format. See Plugin manifest — Validation channels and incoming decisions.

  • Plugins. A plugin whose inbound connection type declares connection declares sdk: "^0.1.8"; a host on contract 0.1.7 refuses it. Plugins in ^0.1.x stay accepted.
  • Journal format. Channels are published and closed only for runs of journal format 2 (RUN_JOURNAL_FORMAT=2 on the proxy). A format 1 run publishes to the patient record only, as before.
  • Runs in flight. A run suspended before the upgrade replays its journal unchanged; its pending validation has no channel publication.
  • API. POST /api/runs/:id/resolve accepts choice for a choice validation; the run view's pending carries message and options.
  • Bundled plugins. @posos/common 0.4.3 adds the human.choose node and an optional validation connection on human.approve and human.choose; @posos/slack 0.1.1 adds the Slack validation channel and its listening connection (see Decide validations in Slack).

Node connections under contract 0.1.7

The host is on contract 0.1.7: each instance of a node designates its connection (nodes.<id>.connection), a node can require a connection by port (connection: { port }), and a connection type can be reached over another protocol than HTTP (urlSchemes) and give its credential to its plugin (exposeCredentials). See Plugin manifest — Connection types.

  • Plugins. A plugin that uses these keys declares sdk: "^0.1.7"; a host on contract 0.1.6 refuses it, so upgrade the host first. Plugins in ^0.1.x stay accepted.
  • Workflows. A workflow with nodes.<id>.connection needs an endpoint on 0.1.7: an older endpoint drops the key and uses the default connection. Roll out the endpoint before publishing such a workflow.
  • Connection messages. The issue on a baseUrl of the wrong scheme reads baseUrl : schéma https ou http attendu..

Notification channels

@posos/notify 0.2.0 makes the channel a connection designated by the notify.send node, and the image ships two channel plugins, @posos/slack and @posos/mail. All three declare sdk: "^0.1.7". See Send notifications.

  • Node configuration. recipients (destinations separated by commas) replaces channel; a channel still set is read as one recipient. The two demo drafts of content/ use recipients.
  • Webhook. Declare a connection of type notification-webhook and bind it (ports.notification = { adapter: webhook, connection: <id> }) or designate it on the nodes. config.url, config.headers, config.timeoutMs and NOTIFY_WEBHOOK_URL are still read with a warning ([notify/webhook] config.url est déprécié …). The JSON body carries recipients instead of channel; with a connection, patientId is sent only when the connection sets patientContext: true.
  • Deployment. A deployment that sets its own PLUGINS_PATH adds /app/external-plugins/slack and /app/external-plugins/mail to load the new channels.

Decision models by connection

@posos/decision 0.2.0 and @posos/typesafe 0.2.0 declare sdk: "^0.1.7". A decision model server is a connection of type typesafe-systemone; a decision.ask node designates it, else it uses ports.decision. Without compatibility:

  • Instance manifest. ports.decision.config.providers and defaultProvider are no longer read, nor TYPESAFE_API_KEY, TYPESAFE_BASE_URL and TYPESAFE_DEFAULT_MODEL. Before the rollout, declare one connection per server (TypeSafe: baseUrl: https://api.typesafe.ai, auth: { scheme: bearer, token: env:TYPESAFE_API_KEY }; a former provider: its baseUrl, defaultModel, timeoutMs, headers, and its key as the connection's auth) and name the default in connectionDefaults.typesafe-systemone. See Configure ports and adapters.
  • Workflows. The node's provider field is removed: a node that still sets it fails with Champ « provider » retiré : désigner la connexion du nœud. Designate the connection of the node instead (nodes.<id>.connection). No workflow of content/ set it.

Inbound reception renamed

The reception subsystem no longer carries the name of the external EAI system. At the proxy's first boot the tables eai_connections, eai_inbox and eai_listener_status are renamed inbound_connections, inbound_messages and inbound_listener_status (data kept; the leader lock key is unchanged, so an older and a newer proxy still exclude each other during a rolling deploy). Without compatibility:

  • event ids derived from a received message start with inbound: instead of eai:; a fact received before the upgrade and again after it starts a new run;
  • the metrics meridian.events.generated and meridian.events.contract_missing carry source: inbound;
  • the OpenAPI tag of HTTP connections is Réception.

INBOUND_DIRECTORY_ROOT replaces EAI_DIRECTORY_ROOT, which is still read when the new variable is unset.

The foreign key from inbound_messages to inbound_connections is dropped: an inbound connection declared in the instance manifest has no row, and its messages reference it by its identifier. Deleting a connection that has messages stays refused by the API.

EAI transports as inbound connection types

A transport is now an inbound connection type with declared parameters: http and directory from the host, mllp from @posos/hl7 0.2.2 (sdk: "^0.1.6"). At the proxy's first boot, the inbound_connections table moves the fields host, port, encoding and directory of each connection into params (idempotent). The API keeps accepting those fields at the top level of a POST or PATCH /api/connections body (the hospital simulator still sends them), and the console builds the connection form from the parameters the type declares. A plugin still declaring receive.transports keeps working: the host turns each transport into an inbound type with the parameters host, port and encoding.

Event sources as inbound connection types

An event source (fhir-poll of @posos/fhir, adsb-poll of @posos/aero-demo) is an inbound connection type: a connection whose type is the source name polls with its parameters and enables its bindings, and each record goes through the reception inbox (inbound_messages) before its binding projects it. See Connect an event source.

  • Instance manifest. sources and eventBindings are deprecated and still read: at boot, each sources.<name> with at least one binding left by eventBindings becomes the read-only inbound connection <name> of type <name>, with the entry as its parameters and, as its bindings, the eventBindings list restricted to the source (absent: every binding of the source). The proxy logs clés sources et eventBindings du manifeste d'instance dépréciées : déclarer une connexion du type de la source sous connections, avec ses bindings. Replace them with a connection per source, and put on each one the bindings of its own source:

    # Before
    sources:
      fhir-poll: { connection: entrepot, intervalSeconds: 15 }
    eventBindings: [fhir-creatinine, fhir-validation-decided]
    
    # After
    connections:
      fhir-poll:
        type: fhir-poll
        connection: entrepot
        intervalSeconds: 15
        bindings: [fhir-creatinine, fhir-validation-decided]
    

    Keep the source name as the connection identifier. The event id of a record is <connection id>:<feed>:<recordId>: the identifier fhir-poll keeps the event ids fhir-poll:<feed>:<recordId> of the earlier releases, so the records re-read in the overlap window after the restart are duplicates. Under another identifier, those records produce new event ids and start new runs.

  • Parameters are checked. @posos/fhir 0.1.8 and @posos/aero-demo 0.2.4 declare the parameters of their source (params). A parameter the source does not declare, a value of another type (a numeric string such as "15" included) or out of bounds makes the connection unusable: the proxy logs connexion entrante « <id> » du manifeste inutilisable : … and the source does not start. sources.<name>.baseUrl is no longer read: the connection gets the issue sources.<name>.baseUrl n'est plus lu : désigner une connexion par le paramètre connection. and does not start; declare a fhir-r4 connection and name it with connection. For adsb-poll, the issue ends with clé réservée aux connexions sortantes.: set the URL with the apiUrl parameter. The plugin contract stays 0.1.6: an earlier host loads these plugin versions without label and params.

  • One replica polls. The sources start on the proxy replica that holds the reception lock (the Postgres advisory lock of the other inbound connections), not on every replica.

  • Records set aside. The table source_deferred, the route GET /api/sources/deferred and the outcome deferred of meridian.events.generated are removed. At the proxy's first boot, the pending rows of source_deferred move into inbound_messages, under the connection named after the source, and the table is dropped; they are delivered only by a connection with that identifier. A record whose delivery fails is delivered again by the reception (delivery-unavailable, outcome unavailable), and shows in the traffic history of its connection. Move the alerts on outcome: deferred to outcome: unavailable.

  • Routing audit. routing:audit has no --instance option: it reads the feeds of the pulled connections from GET /api/connections and GET /api/connections/protocols. See Audit event routing.

  • Health data. The polled records (FHIR resources) are kept in inbound_messages, like HL7 v2 and PN13 messages, without purge. Include the table in the backup and retention policy of the application database (see Back up and restore).

  • Rollback. An earlier release does not read a connection of a source type and polls only the sources under sources: restore that form when rolling back.

Credential reference renamed credentialId

The field that names a credential in a workflow is credentialId, as on an exposed agent (api.credentialId) and a connection: the configuration of a trigger.webhook trigger, of a flow.wait node in webhook mode, and of an EAI connection.

  • At the proxy's first boot, every version of every workflow in the definition store (drafts, published and historical versions, so that a rollback stays authenticated) has its credential key renamed credentialId, comments and layout kept, content hash recomputed; the definition generation is bumped when a row changes. The EAI connections table (inbound_connections.config) is migrated the same way. Both migrations are idempotent and run under a lock, so replicas booting together migrate once.
  • Specs that arrive later with credential (an import, an older client) are renamed when they are saved. A workflow file kept in git with credential is renamed the next time it is seeded; rename it in the repository too.
  • Rollback of the platform. An earlier release reads credential only: after a rollback, webhooks and webhook waits with authentication answer 503 until their workflows are saved again with credential. Back up the definition store before the upgrade (see Back up and restore).

Run settlement

The release that settles runs in the proxy (see Run index — Final status) adds, at the proxy's first boot, the column restate_url to the run index and the partial index runs_live_restate_url_created_at. It fills no existing row.

  • Rows of an earlier release have no restate_url. At each settlement pass the proxy reads how their invocation ended: when its Restate knows the invocation (470, 200, or a failure with a code), the row is stamped with the proxy's RESTATE_URL and settled like the others. When Restate answers 404, the row is left as it is and counted in the warning <n> run(s) d'un autre Restate ou sans Restate d'origine, non réglé(s), at each pass: such a row is never frozen automatically, since nothing says whether this Restate ran it. List these rows, check each run, and set the status of those you know have ended:

    SELECT id, workflow_file, created_at FROM runs WHERE final_status IS NULL AND restate_url IS NULL;
    UPDATE runs SET final_status = 'failed' WHERE id = '<runId>' AND final_status IS NULL;
    
  • A proxy pointed at another Restate than the one of its rows (a wrong RESTATE_URL, an index shared with another stack) freezes none of them: they are counted in the same warning.

  • PostgreSQL: the CREATE INDEX runs inside the init transaction, under the advisory lock, like the earlier indexes. On a large runs table it holds writes on runs while it builds, and the other replicas wait for it at boot. To build it without holding writes, create the column and the index before the rollout; the boot then finds them:

    ALTER TABLE runs ADD COLUMN IF NOT EXISTS restate_url TEXT;
    CREATE INDEX CONCURRENTLY IF NOT EXISTS runs_live_restate_url_created_at
      ON runs (restate_url, created_at, id) WHERE final_status IS NULL;
    

Hermes connection for terminology

@posos/snomed 0.1.3 contributes the connection type hermes and its terminology adapter requires a connection of that type. It declares sdk: "^0.1.6".

  • Instance manifest. Move the Hermes URL (terminology.snomed-ct.config.baseUrl, or HERMES_BASE_URL) to a connection of type hermes under connections:, and the language reference set (config.languageRefsetId, or HERMES_LANGUAGE_REFSET_ID) to its parameter languageRefsetId, written as a string. Name it in connectionDefaults.hermes, or in terminology.snomed-ct.connection. See Configure terminology.
  • Former configuration. The old forms are still read with a warning ([hermes] config.baseUrl est déprécié …, [hermes] HERMES_BASE_URL est déprécié …).
  • No implicit URL. Without a connection, config.baseUrl or HERMES_BASE_URL, the adapter no longer falls back to http://localhost:8081: it logs [hermes] connexion indisponible (…) once and degrades as when Hermes is unreachable (codes accepted, subsumption refused, raw code as label).

5. Verify

  • GET /healthz and GET /readyz on the proxy — /readyz returns 503 with a { checks } detail if the Restate ingress or the run index isn't reachable yet after the restart.
  • Check the endpoint's plugin-load logs (component: "plugins") for the plugin you just published. It should appear in plugins chargés, with no contrat plugin incompatible — plugin ignoré line and none of the other load warnings (« dépendance signalée », « événement signalé », « exemple d'événement retiré », « déclencheur signalé », « nœud signalé », « binding hors contrat », « plage sdk insuffisante », « clé ignorée »), in particular no dropped agent (agent « X » : … agent ignoré) and no dimension outside the dependsOn closure (dimension « X » inconnue dans la fermeture dependsOn), and no chargement échoué (a registry collision, such as a dimension already contributed by another plugin). The repository's plugins load without any of them. A registry plugin that resolved to an older version than the highest of its range is logged under component: "registry" as « plugin requis : versions plus hautes écartées ». See Observability signals.
  • Check that the dashboards and alerts read meridian.* metrics once the new proxy and endpoint export them.
  • Start one new event and confirm the run picks up the new plugin version/behavior, while runs already in flight before the restart keep behaving as journaled.

Releases that change event routing

Some releases change which workflows start on an event, the run ids, or the HTTP contract between the console and the API. Their release notes give the release-specific steps: a pending changeset under .changeset/ until the Version PR runs, then the entry of that version in the library changelogs (libs/shared/CHANGELOG.md and the other libraries of the fixed group). The API changelog (apps/api/CHANGELOG.md, written by release-please from the commit titles) does not carry them. These rules apply to all of them:

  1. Audit the routing of the deployed instance before the upgrade, and again before a rollback — see Audit event routing. Run the audit script of the release of the API it audits: it reads fields of the API of its own release and has no fallback for another one. For each event type that starts several workflows, accept it, set a filter or unpublish.
  2. No publication during the upgrade.
  3. Replace every proxy replica at once when the run ids change: two replicas of different versions ingesting the same event in the same instant would start it under two keys.
  4. Keep the stop shorter than startOverlapSeconds (600 s by default) for each fhir-poll connection; beyond it, FHIR resources modified during the stop are not read. See Restarts and the overlap window.
  5. Deploy the console with the API when the HTTP contract between them changes: the POST /api/events response, the node metadata served by GET /api/catalog (a trigger's receives, or a new trigger family such as configurable, served for « Événement reçu »), or the POST /api/expressions/check request (trigger: { type, config? }). A console released before « Événement reçu » cannot load a catalogue that serves it and shows the core nodes only; a later console leaves out only the node type whose trigger family it does not know. Upgrade the meridian-config-plane image in the same rollout as meridian-api, and downgrade both together. The release notes of the API and the console mark such a release as breaking.

Seeds changed by a release are not re-applied to a populated store. A seed file whose slug the store does not know is seeded; when its event already starts a published workflow, it is stored as a draft, with the log line Graine « <slug> » semée en brouillon : …. Publish it from the console once reviewed. See Manage workflow storage.

Roll back

  • Plugin roll back is just re-pointing the manifest's range at the older version (or leaving a path override in place) and restarting endpoint + proxy — published versions are immutable, so the old version is still sitting in the registry untouched. The runs that the newer version blocked go on once the version that wrote their journal is loaded again (see Recover runs blocked by a missing plugin).
  • Platform roll back is redeploying the previous image tag, after unpublishing the workflows that use a core node the previous image does not know and waiting for their runs in progress to end (see Workflows that use a new core node). The endpoint is not rolled back below a journal format while runs of that format are in flight: set RUN_JOURNAL_FORMAT=1, then roll the endpoint back once the runs of format 2 have ended (see Journal formats). If you also need to roll back the SDK version a plugin was checked against, the two kinds of plugins behave differently. A plugin found on PLUGINS_PATH or through a path: entry whose sdk: range no longer matches is dropped with a warning, not a hard boot failure, so a partial rollback degrades gracefully rather than blocking restart. A registry plugin is resolved again: the highest version the previous image accepts, by its plugin contract and its core plugin versions, is selected at the next boot, and the boot fails when no version of the range fits (see Business plugins that require a newer core plugin). An image older than the metrics rename exports clinical.* again: switch the dashboards and alerts back with it.
  • A host older than plugin contract 0.1.1 refuses every plugin that declares sdk: "^0.1.1". The plugins of the image roll back with it; a registry plugin on ^0.1.1 is skipped with contrat plugin incompatible — plugin ignoré. Its nodes leave the catalogue: the workflows that use them fail validation, and events of its trigger types are refused. Pin such a plugin to a version on ^0.1.0 before rolling the host back.

Multiple-entry workflows

Deploy compatible Restate endpoints on every replica first, then the proxy and console. Multiple entry nodes are available by default, without an activation setting. The run store adds nullable trigger_id and spec_json columns automatically in SQLite and PostgreSQL; no backfill or single-entry YAML conversion is required. Prefer RUN_JOURNAL_FORMAT=2 for new runs: it freezes the entry projection and branch plan.

Before rolling back to a binary that predates multiple entries, drain those runs and unpublish or convert the multi-entry definitions to single-entry workflows. Keep a compatible console to inspect historical multi-entry runs.

97 documents30 sectionssource : /docs · généré au build