Skip to content

Release Notes ​

All notable changes to this project are documented in this file.

The format is based on Keep a Changelog, and this project adheres to a single monorepo-wide version declared in global.json.

[Unreleased] ​

Fixed ​

  • Core: ./gradlew publishToMavenLocal works without a GPG key; signing is only required when publishing to Maven Central. The 0.x releases aren't on Maven Central (it has older versions up to 1.3.0), so the Core Java documentation now explains how to install a release locally.

[0.10.0] - 2026-11-13 ​

Added ​

  • Core: an ActiveDialogue can keep a log of the conversation, as the Web Service does (#483). setLoggedDialogue(log, factory) or keepInMemoryLog() records every agent statement and reply, across dialogues (a reply into another dialogue completes the log and starts a new one from the LoggedDialogueFactory), and completes it when the dialogue ends. back() goes to the previous agent step, ActiveDialogue.restore(...) rebuilds a dialogue from a log at a step, executeCurrentNode shows it again, cancel() marks the log cancelled, and getCurrentMessage() says where the step is in the log. InMemoryLoggedDialogue is a simple log for an application that doesn't store it; the logs of one conversation share a session ID, and each points back to the one before it (getPrevious()). A log is set before the dialogue starts: ExecutableProject.createDialogue(name, language, variables) gives a project's dialogue without starting it.
  • Core: ExecutableProject.load(Path) loads a project from its dlb-project.xml in one call. A project with parse errors fails with a ProjectParseException that lists every error per file, instead of a project that is null. Parse warnings are available from the new ExecutableProject.getWarnings(), also for projects loaded with ProjectParser (#473).
  • Core: ExecutableProject.getDialogue(name) and getDialogue(name, language) look up a dialogue by its name. They use the same language rules as the Web Service: the exact language, otherwise the source language. An unknown dialogue fails with an ExecutionException of type DIALOGUE_NOT_FOUND. getDialoguePointer(name, language) returns the dialogue's ResourcePointer, and getSourceLanguage() returns the project's source language, also for a project without metadata (#473).
  • Core: an ActiveDialogue follows a reply into another dialogue itself. Create it with a DialogueResolver, which finds a dialogue by name and language (an ExecutableProject is one), and call selectReply(replyId, eventTime): it returns the next node, also when that is in another dialogue, keeps the same VariableStore and follows the user's gender there too. A DialogueSwitchListener hears of the switch before the first node there runs (#473).
  • Core: ExecutableProject.startDialogue(name, user) starts a dialogue of the project for a user, with a new VariableStore. Overloads take a language, an existing VariableStore and the start time. The dialogue follows replies into the project's other dialogues, and where a translation depends on the user's gender, it follows $dlb.userGender at every step (#473).
  • Core: ActiveDialogue.getCurrentMessage() returns the current step as a DialogueMessage, the same shape the Web Service sends, ready to show: speaker, statement and replies. ActiveDialogue.isFinished() says whether the dialogue has finished: after a reply to End, or at a node without replies (asked of a node that hasn't been executed yet, it throws an IllegalStateException, as getCurrentMessage() does). The Web Service and the command-line tool use this one rule now (#473).
  • Core: ExecutableProject.generateSummaryString() describes a loaded project, as ProjectParserResult.generateSummaryString() did for a parse without errors (#473).
  • Core: DialogueStatement.getText() returns the text of a statement (its text segments joined, without input fields or actions), as fullStatement() does in the JavaScript client. ReplyMessage.getText() does the same for a reply, and ReplyMessage.isAutoForward() tells an auto-forward reply apart. The JSON the Web Service sends is unchanged (#485).
  • Core: ActiveDialogue.selectReply(replyId), selectReply(reply) (a ReplyMessage of the current step), startDialogue() and startDialogue(nodeId) need no time: they use now, in the time zone of the user of the dialogue's VariableStore (#485).

Changed ​

  • Internal API: Core: DialogueState is removed (#483). An ActiveDialogue restored with ActiveDialogue.restore carries the log and the step itself, and the Web Service uses that instead of its own code for recording, going back and resuming; its REST behaviour, JSON and database are unchanged.
  • Core: Breaking: an ActiveDialogue always has a DialogueResolver, and follows replies into other dialogues itself. The constructor ActiveDialogue(ResourcePointer, Dialogue, VariableStore) is replaced by ActiveDialogue(DialogueResolver, ResolvedDialogue, VariableStore), or start a dialogue with ExecutableProject.startDialogue. processReplyAndGetNodePointer and progressDialogue(InternalNodePointer, ...) are replaced by selectReply(replyId, eventTime), which returns the next node instead of a pointer, also in another dialogue. setUserGenderTranslations is replaced by the user-gender translations of the ResolvedDialogue (#473).
  • Core: the command-line tool (DialogueBranchCLI) plays a dialogue that isn't available in the chosen language in the source language, also when a reply links into it, like the Web Service does. It used to stop with an error. For a language the project doesn't have at all, --execute prints a warning and plays the source language (#473).

Fixed ​

  • Web service: /dialogue/progress and /dialogue/back with a step that is not in the dialogue's log answer 404, instead of a generic 500 (#483).
  • Web service: /test-session/progress with a reply that is not in the current step answers 404, instead of a generic 500.
  • Core: ActiveDialogue.selectReply throws an ExecutionException instead of a NullPointerException for a reply that is not in the current node (REPLY_NOT_FOUND) or a dialogue that has finished (DIALOGUE_COMPLETED), and refuses a reply once its log has been cancelled or completed, instead of recording into it; it changes nothing then.
  • Studio: after a reply that led into another dialogue, the Dialogue Workspace kept showing the log ID of the first dialogue, and the Dialogue Browser could open an ongoing dialogue in a second tab. It now follows the new dialogue's log (#473).
  • Core: the command-line tool kept no variables when a reply led into another dialogue: a variable set in one dialogue was empty in the next. It now plays them as one run, with the same variables throughout (#473).

[0.9.2] - 2026-11-11 ​

Added ​

  • Web service: GET /variables/list-project also names the dialogues that read and write each variable, in two new fields readBy and writtenBy, so a client can show the variables of one dialogue. The new fields are added to each entry, the existing ones are unchanged. DialogueBranchAuthoringClient.listProjectVariables returns them too.

Changed ​

  • Web service: GET /variables/list-project is now a stable end-point, covered by the compatibility policy, and documented with the other variable end-points. A service that needs to know which dialogues set which variables can now build on it.

Fixed ​

  • Web service: GET /variables/get, POST /variables/set, POST /variables/set-single, GET /dialogue/list-dialogues and GET /dialogue/get-ongoing answer a project that doesn't exist with a 404 (Project not found: <slug>), like the other project end-points. The variable end-points answered a 500, and the dialogue end-points an empty answer (#467).

[0.9.1] - 2026-11-04 ​

Security ​

  • Web service, BFF and mock variable service: upgraded to Spring Boot 4.1, which fixes critical vulnerabilities in Tomcat, Spring MVC and Spring Security.
  • Studio: updated dompurify, vue and vite, and the Docker image now serves the build with nginx on Alpine instead of Apache httpd on Debian.

[0.9.0] - 2026-11-04 ​

Added ​

  • Core and web service: a project can define its speakers, each with a name, a gender (male, female or undefined) and whether it is the default for new nodes (#393, #394). They are an optional <speakers> section in dlb-project.xml, which core reads and writes (ProjectMetaData.getSpeakers(), Speaker), refusing an empty, duplicate or reserved (_user) name, an unknown gender or more than one default. The web service keeps them as a draft and per published version, like translation languages: get-project returns draftSpeakers and latestVersion.speakers, publishing snapshots them, export and import carry them, and discarding all draft changes restores them. A new database migration (V12) adds the tables. Internal API:POST /project/update-draft takes speakers, the project's complete new list of speakers.

  • Studio: the project's speakers in Configure Project and the node editor (#395, #396). A new Speakers tab adds and removes speakers, sets their gender and the default speaker, saved with the rest of the project's draft configuration. When a project has speakers, the node editor picks a node's speaker from them (keeping, and marking, one that isn't), a new node gets the default speaker, and a node other than End can't be saved without a speaker. Internal API:DialogueBranchAuthoringClient.updateProjectDraft takes speakers.

  • Core, web service and Studio: a warning for a node whose speaker isn't one of the project's speakers (#397). Core's ProjectParser reports it with its other warnings when the project defines speakers (a new constructor takes them for a loader without a dlb-project.xml); it never stops a project from parsing, publishing or running. POST /publish/verify now also returns the warnings per dialogue, which it dropped before, such as orphaned nodes. Studio's publish wizard lists them, and the graph editor and node editor mark a node with an undefined speaker.

  • Web service and Studio: renaming a speaker renames it in the project's nodes and translations too (#398). In Configure Project, a rename is saved with the rest of the project's draft, after confirming how many nodes, dialogues and translations it changes. Internal API: a speakers entry of POST /project/update-draft takes previousName to rename that speaker; the new GET /project/find-speaker-references counts what a rename changes; DialogueBranchAuthoringClient.findSpeakerReferences.

  • Core and web service: translations use the speakers' genders (#399). A male or female speaker of the project selects its male_speaker/female_speaker translations, in live playback and in tests; an undefined gender (now also the default) selects a gender-neutral translation where there is one, else the speaker's first. Core: TranslationContext.Gender.UNDEFINED, TranslationContext.forSpeakers.

  • Core and web service: the end user's gender, the reserved variable $dlb.userGender (male, female, undefined or empty), selects male_addressee/female_addressee translations, the new male_user/female_user ones (the user wherever they are in the text, also when only talked about), and those of the user's own replies (#400). It is read at every step, in live playback and in tests; a dialogue whose translations depend on it is translated for each gender when first needed. Core: ExecutableProject.getDialogueForUserGender, ActiveDialogue.setUserGenderTranslations.

  • Studio: gender-specific translations (#453, #454, #455). The Translation Editor adds a Female user and a Male user translation to a text (stored as female_user/male_user), removes a gender variant in every language, shows each speaker's gender, and marks a variant that can never be used. Test in [language] as [any user / a female user / a male user] sets $dlb.userGender in the test values.

Changed ​

  • Breaking: variable names starting with dlb. are reserved for variables with a predefined meaning (#400). Setting one that isn't defined, or a defined one to a value it doesn't allow, is refused: a <<set>> with a fixed value, or an input command storing into one, is a parse error; POST /variables/set-single, POST /variables/set, a reply's input and the test variables answer 400; a value from the External Variable Service is skipped and logged. Rename any variable of your own that starts with dlb.. Core: ReservedVariables; VariableStore throws IllegalArgumentException for a refused value.
  • Breaking: the reserved node header tag colorId (and its legacy spelling colorID), a palette index, is replaced by nodeColor, a colour written as #rrggbb (#203). Studio's node editor offers the same ten presets plus any custom colour. Existing content isn't converted: an old colorId tag is kept as an ordinary custom tag, and the node shows the default colour until it is given one. Dialogues run as before (the tag never affected execution). To keep the old colours in .dlb files, replace colorId: N (or colorID: N) with nodeColor: and the Nth of #c2410c, #0d9488, #7c3aed, #0369a1, #be123c, #65a30d, #a16207, #4f46e5, #0891b2, #be185d (counting from 0).

Fixed ​

  • Studio: moving several selected nodes together in the graph editor saves every node's new position, not only the one that was dragged (#196); the others went back to where they were the next time the dialogue was opened.
  • Studio: saving Configure Project no longer replaces the project's name in the header with its slug.

[0.8.1] - 2026-10-30 ​

Fixed ​

  • Web service: requests of the same user that arrive at the same time, such as a double tap, a retry racing the original or two open tabs, no longer overwrite each other (#284, #216). Two steps of one dialogue could each save over the other, losing one, and a dialogue step that changes a variable, such as <<set $count = $count + 1>>, could lose a change. Starting, progressing, continuing, going back in and cancelling a dialogue, and setting variables, now run one at a time per user and project; other users and reading aren't held up.
  • Web service: the first requests of a new user arriving at the same time could fail with a 500 error and leave the user with two diverging in-memory sessions (#214); a user now always gets one.
  • Web service: creating, changing or deleting a draft node or translation saved its content and then marked the dialogue as changed in two separate steps, so a failure between them left the dialogue's Draft status wrong (#217); both now happen together or not at all.
  • Web service and Studio: importing a project from a zip made with macOS Finder's Compress failed validation (#444). The import now leaves out what an operating system adds to an archive for its own use (everything under __MACOSX/, ._ files, .DS_Store, Thumbs.db and desktop.ini), and also finds the project when the archive holds nothing but one folder with it, as when you compress the project's folder rather than its contents.

[0.8.0] - 2026-10-30 ​

Added ​

  • Studio and web service: Studio shows whether the configured External Variable Service is running (#430). The status bar says "External Variable Service: connected" or "not reachable" (nothing when none is configured), and admins find its URL, last successful and failed calls, last error and failed calls in a row in Technical Information. The Variable Browser's Used list also says when the service can't be reached. Internal API: a new GET /info/external-variable-service (editors and admins) checks the service with one GET /variables/supported call, at most every 30 seconds; any answer but a 401 or 403 counts as running. /info/technical gains externalVariableService, and client-js gains getExternalVariableServiceStatus.
  • Studio: the Variable Browser shows the hints an External Variable Service gives for its supported variables (#434). In Used, a variable shows its type and a lock when it is read-only, with its description and allowed values in the tooltip. In Values and Test values, a lock marks a read-only variable, and its tooltip says what changing it does: only the test values change, or the service's value replaces the change at the next dialogue start.
  • Web service, client-js and mock-variable-service: an External Variable Service can give each variable it reports in GET /variables/supported the optional hints type (number, string or boolean), values (the allowed values) and readOnly (#433). GET /variables/list-supported passes them on as sent, and client-js's listSupportedVariables now returns SupportedVariable objects (also exported from @dialoguebranch/client-js/protocol) carrying them. The hints are for authors only and aren't enforced. mock-variable-service declares its three variables as read-only strings, with dayPart's allowed values, and adds four example.* variables that show each hint once; it keeps their values per user and project in memory, starting with a random value of the declared type and changing when a dialogue changes them. It now only answers retrieve-updates for the variables it reports as supported: it no longer returns other variables half of the time with their value unchanged.
  • Studio: a search field on the project page, filtering projects by name, slug or description.
  • Studio, web service and client-js: simulate a user, to investigate a problem they have without changing any of their data (#411). Set Delegate User now offers Simulate as (the default) and Act as. While simulating, dialogues run as test sessions against a copy of the user's variables, on the published content in Live Mode and on the draft in Authoring Mode, with a notice and Stop simulating; the user's real ongoing dialogue isn't offered to resume. Switching between Live and Authoring Mode now stops simulating or acting as someone. POST /test-session/start takes content=published to run a dialogue's published version; client-js's startTestSession takes content.
  • Studio, web service and client-js: deleted projects whose users' data was kept are listed for admins in a separate, collapsed Deleted projects section on the project selection page, with how much users' data they still have, and can be deleted permanently from there, user data and all, after typing the slug to confirm (#403). New GET /project/list-deleted-projects and POST /project/purge-deleted-project; client-js gains listDeletedProjects and purgeDeletedProject.
  • Web service and client-js: an admin can erase everything stored about a user — their variables, variable history and dialogue logs in every project, and the user itself — with POST /users/erase-user (new admin-only USER_ERASE permission); a configured External Variable Service is sent notify-cleared for each project the user had variables in (#320). client-js gains eraseUser.
  • Studio: a Users page for admins, opened from the project selection page, lists everyone the web service has seen with how many variables and dialogue logs it holds for them, and erases a user's data after typing their username to confirm (#407).

Changed ​

  • Documentation: a new Compatibility Policy page lists which surfaces are stable (the .dlb language, the stable REST end-points, authentication, the External Variable Service protocol, DialogueBranchClient, configuration and upgrades, dlb-core-java's public API) and which are internal to Studio. From here on, only a change to a stable surface is labelled Breaking:; a change to an internal one is labelled Internal API:. Release notes up to 0.7.0 are unchanged.
  • Breaking: Language: variable names may contain dots, so $settings.theme is one variable (#432). A dot belongs to the name only when a letter or underscore follows it, so $score. at the end of a sentence is still a variable followed by a full stop. The rule is the same in text, replies, quoted strings and <<set>>/<<if>> expressions. As a side effect, $m.key in an expression no longer reads a field of a map held in $m (an undocumented behaviour of the expression engine) but the variable m.key. The Web Service's variable end-points accept the same names (and now also a leading underscore, as scripts always did). In dlb-core-java, the rule is com.dialoguebranch.model.common.VariableNames.
  • Breaking: Web service: the Web Service authenticates to the External Variable Service with an OAuth2 access token instead of a static API key (#428). It gets the token from Keycloak with the client-credentials grant, caches it and fetches a new one shortly before it expires, and sends it as Authorization: Bearer on every call. The dlb.external-variable-service.api-key setting is gone; new settings dlb.external-variable-service.oauth2.token-uri, client-id, client-secret and optional scope replace it, and the Web Service does not start without them while the integration is enabled. An External Variable Service must check the token's signature, issuer, expiry, audience and role, as described on its documentation page; mock-variable-service does, and the Docker stack's realm has the clients and role for it. Failing to get a token is handled like any other failed call and never breaks a dialogue.
  • Breaking: Web service: a variable updated by the External Variable Service is stored as updated when the Web Service received it, as for every other source, so /variables/get returns that time as its updatedTime (#321). The retrieve-updates response now carries each variable's name and value only; an updatedTime or updatedTimeZone it still sends is ignored. The request still sends each variable's updatedTime, which now means the Web Service has held that value since then. mock-variable-service answers with name and value only.
  • Web service: a new project's starter dialogue has a preamble saying it was generated automatically.
  • Studio: Live Mode can only be chosen once a project has a published version; an unpublished project opens in Authoring Mode.
  • Studio: the header's mode switch shows Live and Authoring side by side and switches in one click, and a mode bar under the header says which mode you're in, in its colour (amber for Live, green for Authoring). Simulating or acting as another user is shown in the mode bar too, which turns red while acting as them, instead of in a banner above the dialogue.
  • Studio: the Dialogue Workspace's view switch is in its title row, with a label on each view (Test (speech bubbles), Test (text style), Edit Dialogue, Translate Dialogue), and its other controls are in a toolbar above the tabs. Opening Edit Dialogue no longer collapses the Variable Browser, and the workspace's own reset of the test values is gone: the Variable Browser has one.
  • Studio: the Dialogue Browser can be collapsed, like the Variable Browser; both fold away with a button on their edge next to the Dialogue Workspace, and stay collapsed after a reload.
  • Studio: the Dialogue Browser, Dialogue Workspace and Variable Browser line up, with restyled panel titles and a grey toolbar row at the top of each panel; the Dialogue Browser's and Variable Browser's buttons, view switch and sort order moved into their toolbars, with sorting in a small menu.
  • Studio: a refreshed look for the participant view and the project and user pages: a shared header with the logo, a "Studio" wordmark, a divider and a greeting; for editors and admins a menu bar to switch between Project Management and User Management (admins only), replacing the Users and ← Projects buttons; and the page content in a white box with rounded corners. The Service Unavailable, Update Available and Insufficient Privileges screens use the same logo.
  • Studio: acting on behalf of another user for real (Act as) is treated as the powerful operation it is (#330): choosing it warns that the user's dialogue logs, variables and history change as if they acted themselves, the confirmation says Act as <user>, and Studio shows in red while acting, with Stop acting as. Set Delegate User is shown to admins only.
  • Docs: the Authentication page no longer describes a trusted server that acts for all of its end users through one admin account and delegateUser (#330). A backend serving many users should forward each user's own access token instead; delegateUser is for an administrator's testing and support work, described in a new section on acting on behalf of another user.
  • Web service: test sessions read from the External Variable Service like real playback does (#411): at every dialogue start, also through a reply into another dialogue, with the values of the session's copy of the variables. The answers go into the copy only, and a test session still never notifies the service. Every retrieve-updates request now carries testSession (true or false), so an External Variable Service can tell test sessions from real playback; the External Variable Service page explains where requests for updates come from.
  • Internal API: Web service, client-js and Studio: testing a dialogue no longer changes the user's real variables (#319). A test session runs against the tester's own copy of the variables, kept in memory and seeded from the stored ones: it is never saved, recorded in the variable history or sent to the External Variable Service. All of a tester's test sessions in a project share one copy, which GET /test-session/variables, POST /test-session/set-variable and POST /test-session/reset read, change and reset. Testing as another user (delegateUser on start) only reads their variables. The end-points are renamed from /draft/* to /test-session/* (draftSessionId becomes testSessionId, revert-variables is replaced by reset, and progress and cancel no longer take delegateUser), and the permission DIALOGUE_DRAFT_TEST becomes DIALOGUE_TEST. In client-js, startDraftDialogue, progressDraftDialogue, cancelDraftDialogue and revertDraftVariables become startTestSession, progressTestSession, cancelTestSession and resetTestVariables, plus getTestVariables and setTestVariable. In Studio's Authoring Mode, the Variable Browser's Test values shows and edits the test copy, with a button to reset it to the stored values.
  • Breaking: Web service and Studio: deleting a project removes its dialogues, translations and version history, but keeps its users' variables, variable history and dialogue logs by default (#315). The project stays as a deleted project (no longer found or listed) under the slug <slug>__deleted, with its logs moved along, so the original slug is free right away. POST /project/delete-project takes deleteVariables and deleteLogs (both false by default) to delete that data too; with both, the project is removed for good. Studio's delete confirmation offers both as unticked options. client-js's deleteProject takes them as options. A new database migration (V11) adds the deleted flag.
  • Internal API: Web service and client-js: GET /users also lists users with no known username (only ever seen as a delegate, username is null), after the named ones; its username filter is replaced by search, which matches the username or the subject; and each entry has variables and dialogueLogs counts (#407). client-js's listUsers takes search instead of usernameFragment. Studio's delegate user picker finds these users too.

Fixed ​

  • Core: a translated node no longer shows a variable before a <<set>> that comes before it in the source (#437). Translating moved every <<set>> and <<action>> behind the translated text, so the text showed the variable's old value. A command before the text now keeps its place, and text with no translation keeps its source order exactly. A command in the middle of a translated sentence still follows it, since a translation can't say where inside its own text the command belongs.
  • Web service: a slow or unreachable External Variable Service no longer breaks or stalls a dialogue (#219). Every call to it now has a connect and a read timeout, new settings dlb.external-variable-service.connect-timeout (default 2s) and read-timeout (default 5s). A failed retrieve-updates lets the dialogue start with the values it has, and a failed notify-updated or notify-cleared is logged while the change, clear or erase stays in effect; before, a failed notification could throw in the middle of a dialogue turn, and a hanging service could block it indefinitely.
  • Web service: a request the caller got wrong is answered with its own 4xx and the reason, instead of a generic 500 Internal Server Error (#220): a missing required parameter or upload part (400), a parameter of the wrong type (400), an unsupported content type (415) or method (405) and an upload that is too large (413). An upload that arrives without its file, for example because a proxy didn't pass the body on, now says so. Real server errors still answer 500, without details.
  • Studio: the Documentation link in the user menu pointed to a page that no longer exists; it now opens the documentation site.
  • Studio: deleting, adding or renaming a dialogue collapsed every folder in the Dialogue Browser; open folders now stay open.
  • Studio: a long page, such as the Users list, also got a horizontal scrollbar.
  • Studio: blank lines between paragraphs of a dialogue's script comment (its preamble) were dropped on save; they're now kept, as bare // lines.
  • Studio: clicking outside a dialog, such as New Project or the node editor, closed it, losing what was typed; dialogs now close only with their close button or Cancel.
  • Studio: when the participant view's dialogue doesn't exist, a participant now gets a plain message instead of a technical error with a Try Again button that couldn't help.
  • Web service and Studio: test sessions stayed in memory, each with its own parsed copy of the project, until the web service restarted, unless they were cancelled (#218). A test session now ends when its dialogue does, and sessions idle for longer than dlb.test-session.idle-timeout-minutes (default 30), and copies of the variables no session uses, are removed. Studio says so when a test has expired, with a button to start it again. Two requests on the same test session at once could also corrupt it; a session now handles one request at a time (#215).
  • Web service: a request made on behalf of another user (delegateUser) that runs a dialogue or sets variables overwrote that user's time zone with the caller's, so the user's own next dialogue steps and variable changes were stamped in the wrong time zone (#331). It now leaves the user's time zone alone and stamps what it changes in it; its timeZone parameter is ignored.
  • Web service: deleting a project failed as soon as any user had a variable in it (#315); a new project with the slug of a deleted one could inherit its users' cached variables (#316) and its dialogue logs, including an ongoing dialogue to resume (#317); and a deleted project stayed listed and playable until the web service restarted. Deleting a project now succeeds, unloads it, and keeps its users' data away from a new project with the same slug.
  • Studio: a delegate user set in one project stayed active, without being shown, after closing it and opening a project again; opening a project now clears it (#407).

[0.7.0] - 2026-10-15 ​

Added ​

  • Studio: delete a dialogue (#390). Delete… in a dialogue's menu in the Dialogue Browser (Authoring Mode) marks it for deletion after a confirmation that warns how many replies in other dialogues link to it; it can be restored until the next publish, and its open tabs close. Studio had no way to delete a dialogue before, although the web service supported it.
  • Studio, web service and client-js: discard all of a project's draft changes (#364). Discard All Draft Changes… in the Project menu makes the whole draft identical to the latest published version again: every dialogue goes back to its published content and name, dialogues that were never published are removed, dialogues pending deletion are restored, and the project's name, description and translation languages go back to the published ones. The new POST /project/discard-draft-changes needs the permission to publish, and refuses a project that was never published. client-js gains discardAllDraftChanges.
  • Studio, web service and client-js: discard a draft dialogue's changes (#363). Discard changes on a dialogue marked Changed in the Dialogue Browser (Authoring Mode) makes its draft identical to its published version again: nodes and their layout, preamble, translations, and its published name if it was renamed; it is then no longer marked as changed. The new POST /authoring/discard-dialogue-changes refuses a dialogue that was never published or is pending deletion, a published name another dialogue has taken, and a translation language renamed since publishing. client-js gains discardDraftDialogueChanges.

Changed ​

  • Studio: the Dialogue Browser's dialogue rows show only their ▶ (open) button; the other actions (Open, Rename…, Export as .dlb, Discard changes…, Delete…) are in a context menu, opened by right-clicking a row or with a ⋯ button shown when pointing at it (always shown on touch screens) (#389). The menu, also used for the node editor's Insert statement menu, works with the keyboard and stays inside the window. A dialogue pending deletion keeps its Restore button.

[0.6.0] - 2026-10-15 ​

Added ​

  • Studio: the Translation Editor shows translations it used to hide (#386). A translation stored under a source text that differs from the dialogue's text only in whitespace now shows in its row, and editing it replaces it instead of adding a second entry; gender-specific translations (nested context groups such as female_addressee) show as editable sub-rows under their text.
  • Studio: the Translation Editor shows a dialogue's stale translation entries and lets you delete them (#375). Entries that match no text of the dialogue on speaker and text (for example after the text changed) are never used and were invisible; a Stale entries (N) button now lists them per translation language, with their speaker and any gender contexts, and deletes one, or all of a language's, after a confirmation.
  • Studio, web service and client-js: import translation files (.json) into a translation language of an existing project, as draft translations (#340). Import Translations in the Project menu (Authoring Mode) matches each file to the existing dialogue named after the chosen folder and the file name, shows which already have a translation in the chosen language, and only replaces those when asked; open Translation Editors of those dialogues reload. The new POST /authoring/import-translations imports all or nothing, refuses files that aren't valid translation files (the check of update-translation, #376), contain no translations, or have no dialogue (or one pending deletion), stores each file re-serialised, and reports per file how many entries match no term of the dialogue (by speaker and text, kept anyway) and how many terms stay untranslated. list-dialogues now lists, per dialogue, the translation languages it already has a translation in (translatedInto). client-js gains importTranslations.
  • Studio, web service and client-js: export a single dialogue as a .dlb file (#341). Export as .dlb on a dialogue in the Dialogue Browser (Authoring Mode) downloads its current draft: the preamble and nodes as an author sees them, not the published version, without translations. The file is named after the dialogue without its folder, and imports again with Import Scripts as the same dialogue. The new GET /authoring/export-dialogue needs the editor or admin role; client-js gains exportDialogue.
  • Studio: the Variable Browser's History view can be filtered by a date range (#371). Pick a Start date and an End date (whole days in your time zone, the one the entries are shown in; the end date is included), or use a preset: today, the last 7 days or the last 30 days. It combines with the exact-variable-name filter and Load more, and a start date after the end date is reported in the panel instead of being sent.
  • Web service and client-js: GET /variables/history accepts optional from and to (epoch milliseconds) to return only the changes recorded in the range [from, to) (#352). from is inclusive and to exclusive, so consecutive ranges never overlap or leave gaps; either can be left out, and they combine with variableName and paging. A from after to, or a value that is not a whole number, answers 400. A new database migration (V10) adds indexes for the range lookup. DialogueBranchClient.getVariableHistory accepts from and to (a number or a Date).
  • Studio and web service: import .dlb scripts into a folder of an existing project, as draft dialogues (#339). Import Scripts in the Project menu (Authoring Mode, next to Import Translations, #340) previews the resulting dialogue names, shows which already exist, and only replaces those (keeping their translations, and restoring one pending deletion) when asked; open tabs of replaced dialogues reload. The new POST /authoring/import-scripts imports all or nothing, refuses files that aren't UTF-8 .dlb files of at most 1 MB, don't make a valid dialogue name, or can't be stored as draft nodes, and reports syntax errors per file without refusing them. client-js gains importScripts.

Changed ​

  • Breaking: Core: a translation entry only applies to text of its own speaker (#377). The Translator used an entry of any speaker when the speaker had none for a text, and, through a bug in its gender filter, even when it had one, so two speakers saying the same text got the same translation. A translation file's top-level keys are now read as speaker names as a whole (multi-word names such as Martin McOwl were split into separate words that never matched), nested context groups add to their speaker's entries instead of replacing the speaker, and an entry outside a speaker group is a parse error. A text without an entry of its own speaker stays in the source language. In dlb-core-java, ContextTranslation gains a speaker component. The translation file format is now documented in the project reference.

  • Breaking: Language: the speaker name _user is reserved (#383). Translation files use it for the user's replies, so a node with speaker: _user collided with them; the parser now refuses it (except on an End node, whose speaker is ignored), and Studio's node edit panel refuses it before saving. In dlb-core-java, the reserved name is DialogueBranchConstants.DLB_USER_SPEAKER, which replaces SourceTranslatable.USER.

Fixed ​

  • Core: a translation containing a command other than <<input>> (for example <<if>> or <<set>>) made TranslationParser crash instead of reporting a parse error (#379), so loading, verifying, publishing or draft-testing a project with such a translation failed with an internal error. It is now reported as an invalid translation string, naming the command.

  • Web service: update-translation stored any content it received, so invalid content was only noticed later, when testing a dialogue or publishing the project failed (#376). It now refuses content that isn't a valid translation file, including entries outside a speaker group, with the problems listed, and stores valid content re-serialised, without empty translations.

  • Studio: importing a project archive (Import Project) failed, because the BFF passed the upload on to the web service without its contents (#339). The BFF no longer handles multipart requests itself (its CSRF check made the servlet container read the request body first), so uploads now reach the web service unchanged.

  • Studio: a collapsed dialogue preamble now leaves a small comment icon on the graph canvas. Hover shows the preamble; clicking expands it again (#361).

[0.5.0] - 2026-10-14 ​

Changed ​

  • Breaking: Web service: the authoring end-points exchange a draft node's header as tags instead of text (#350). list-nodes, create-node, update-node and rename-node return a node as {id, title, tags, body, createdAt, updatedAt}, where tags is an ordered list of {key, value} pairs, instead of with a header string. create-node and update-node take {tags, body}: create-node no longer has a separate title (the node's title is its title tag, which must be a valid node name), and update-node replaces all of a node's tags. Tags that don't form a valid header (an invalid key, a key used twice, a value on more than one line) are refused with a 400, and so is a script whose node headers aren't valid tags when it is stored as draft content (for example in a project import). In client-js, createDraftNode(projectSlug, dialogueName, tags, body) and updateDraftNode(projectSlug, dialogueName, nodeTitle, tags, body) take tags, and renameDraftNode resolves to {node, referencesUpdated}. Studio no longer reads or writes header text itself.

  • Breaking: Language: a node header is now a model, a list of key: value tags and nothing else (#343). // no longer starts a comment in a header: it is part of the value, so image: https://... needs no escape there. A trailing comment on a header line (title: Start // entry point) now makes the value invalid, and a comment line inside a header is an error ("Comments are not allowed in a node header"); document a node with a comment tag or a body comment, and a script with its preamble. Header keys must consist of letters, digits, _ and -. Body comments are unchanged. Header reading and writing is defined once, in core's new DialogueBranchHeader, which the parser and the Web Service use. Existing stored content is converted automatically: a migration removes header comments from draft and published dialogues (the comment lines at the start of a dialogue's first draft node move to its preamble), which doesn't change what any dialogue does.

  • Breaking: Web service: /authoring/create-node and /authoring/update-node refuse (400) a header whose title isn't the node's title, or that has none (#343). A node's title changes only through /authoring/rename-node, which keeps its title and header in step.

Added ​

  • Studio: a dialogue's preamble (the comment block at the start of its script) is shown at the top of the graph editor, collapsible, and can be edited there as plain text: Studio adds and removes the // prefixes, and empty lines are left out (#346). Dialogues without one offer "Add script comment". Web service: /authoring/list-dialogues includes each dialogue's preamble, and a new POST /authoring/update-preamble replaces it (refusing lines that aren't // comments); client-js gains updateDraftPreamble.

  • Studio: a node's comment tag can be edited in the node edit panel, and a node with a comment shows a speech-bubble icon on its card in the graph editor, with the comment on hover (#348). Emptying the field removes the tag.

  • Language: a reserved comment header tag, a one-line note about a node for authors (#347). It has no effect on how a dialogue runs, isn't sent to clients playing a dialogue, and isn't translated. Studio shows and edits it (see below).

  • Language: a script's preamble, the // comment lines at the very start of a .dlb file, before the first node (#345). It belongs to the dialogue rather than to its first node: core's Dialogue model and DialogueBranchScriptSplitter expose it, and the Web Service stores it with the draft dialogue (a new draft_dialogues.preamble column) and keeps it at the top of the script when publishing, exporting and importing. Previously such comments ended up in the first node's header in Studio's drafts, where moving that node in the editor deleted them.

  • Core: DialogueBranchScriptSplitter, which splits the text of a .dlb script into its raw nodes (header and body text) and its preamble, and joins them back, by the same separator rules as DialogueBranchParser (#342). It accepts any text and never fails; deciding whether the result is a usable dialogue is left to the caller.

Fixed ​

  • Studio: moving a node in the graph editor, or saving it in the node edit panel, deleted the comments in its header, and a header value containing // was cut off there (#343). Headers no longer contain comments, and Studio keeps // as part of a value.

  • Web service: importing a project (and seeding one at startup) could store a valid .dlb script wrongly as draft nodes, without any error (#342). Scripts with Windows (CRLF) line endings, or with whitespace or a // comment on a --- or === line, came out with no nodes at all, or with several nodes merged into one; a script without a final newline kept a stray === in its last node. Draft nodes are now split by the same rules the parser uses, and a script that still can't be stored as draft nodes (a node without a --- line or a title) is rejected with the dialogue name and line number, instead of nodes being dropped silently.

[0.4.0] - 2026-10-13 ​

Added ​

  • Web service: a new supervisor role (#326) for users who follow other users' progress without administering anything. It has everything participant has, plus a new USER_DELEGATE_READ permission: reading another user's variables, variable history and dialogue logs through delegateUser on the read-only end-points (/variables/get, /variables/history, /log/get-session, /log/verify-id). It can never act on another user's behalf; that still requires USER_DELEGATE (admin), which now also includes USER_DELEGATE_READ. The bundled Keycloak realm import gains the supervisor client role.

Fixed ​

  • Web service: starting a dialogue in a language the project doesn't have (e.g. nl for a project's nl-NL) still runs it in the project's source language, as before, but now logs a warning naming the requested and available languages, so the mismatch no longer goes unnoticed (#335). Previously the language check never ran at all for projects loaded from the database.
  • Studio: the participant view (for users with only the participant role) failed to start its dialogue with a 500 for every user, because it starts it without a language, which /dialogue/start required (#333). /dialogue/start's language is now optional: left out or empty, the dialogue starts in the project's source language.
  • Web service: a delegated read (/variables/get, /variables/history, /log/get-session, /log/verify-id with a delegateUser naming another user) no longer changes anything about that user. It used to overwrite their in-memory time zone with the caller's, so their next dialogue steps and variable changes were stamped in the wrong time zone (#327), and it created a users row for any subject named, including mistyped ones, while returning empty results (#328). It now reads stored data directly, and answers 404 with code UNKNOWN_USER for a user the service doesn't know.

[0.3.0] - 2026-10-11 ​

Added ​

  • Web service: Dialogue Branch Variable value history (#259). Every change to a user's variables (from a <<set>>, an input reply, /variables/set*, or the External Variable Service) is now also appended to a new variable_history table (migration V7), one row per variable per change, with null recording a removal. Read it with the new GET /variables/history end-point (projectSlug, optional variableName, page/pageSize; newest first), gated by a new VARIABLE_READ_OWN_HISTORY permission held by every role. Each entry is stamped with the Web Service's own clock at the moment of recording. Each entry also carries an id, increasing in recording order, so a client paging through the history can de-duplicate entries when new ones shift the pages (#323).
  • Client JS: DialogueBranchClient.getVariableHistory({ projectSlug, variableName, page, pageSize }) reads the new GET /variables/history end-point, returning VariableHistoryEntry instances (new model type, also exported from ./protocol) (#323).
  • Studio: a History mode in the Variable Browser (Values · Used · History) (#323), listing every recorded change to the current user's variables in the project, newest first, with "Load more" paging. Each entry shows the value (or removed), its source, and when it was recorded, in the Studio user's own time zone. Search and sort apply: a search naming a known variable exactly is answered by the server, so that variable's full history pages correctly; partial text filters the loaded entries, with a hint while older ones aren't loaded yet. Sort is newest/oldest first. Clicking a name narrows the list to that variable. It follows the delegate user and refreshes after every dialogue step and variable change, and is only requested while History is showing.

Changed ​

  • Breaking: Core: VariableStoreChange.Clear now carries the names of the variables it removed (getVariableNames()), like Remove already did, so a listener can tell what a clear removed (#259). Its constructors take that collection as a new first argument: Clear(Collection<String>, ZonedDateTime[, VariableUpdatedSource]).
  • Breaking: Core: the Command hierarchy (ActionCommand, SetCommand, IfCommand, RandomCommand, InputSetCommand, InputVariableCommand and its subclasses) is immutable now, with dead setters removed (#306, part of #210's "immutable runtime model" cleanup — an investigation pass found most of the hierarchy's setters were already unused, including at parse time, so this landed as a smaller change than #210 originally scoped). Highlights:
    • ActionCommand.setType()/setValue()/setParameters(), SetCommand.setExpression(), IfCommand.setIfClauses()/addIfClause()/setElseClause(), IfCommand.Clause.setExpression()/setStatement(), RandomCommand.setClauses()/addClause(), RandomCommand.Clause.setWeight()/setStatement(), InputTimeCommand.setGranularityMinutes()/setStartTime()/setMinTime()/setMaxTime(), InputSetCommand.setOptions()/Option.setVariableName()/Option.setText(), InputVariableCommand.setVariableName(), InputNumericCommand.setMin()/setMax(), and InputAbstractTextCommand's eleven setters are all removed.
    • IfCommand/RandomCommand/InputSetCommand lose their no-arg constructor in favor of a full constructor (IfCommand(List<Clause>, NodeBody), RandomCommand(List<Clause>), InputSetCommand(List<Option>)); InputSetCommand.Option similarly loses its no-arg constructor in favor of Option(String, VariableString).
    • InputNumericCommand's single-arg constructor is replaced by InputNumericCommand(String, Integer, Integer).
    • InputAbstractTextCommand's eleven optional fields (length bounds, capitalization/character hints) move to a new InputAbstractTextCommand.Builder, populated via InputAbstractTextCommand.parseAttributes(Builder, ...) (previously parseAttributes(InputAbstractTextCommand, ...)) and passed to InputTextCommand/InputLongtextCommand's constructor — replacing their previous single-arg constructor plus eleven individual setter calls.
    • Translator no longer mutates a cloned IfCommand/RandomCommand's clauses in place during translation — it rebuilds a new one via the constructors above, same as it already did for NodeBody/Reply.
  • Breaking: Core: NodeBody is now immutable, and execution now produces a separate ResolvedNodeBody instead of the same NodeBody type — the result of NodeBody.execute() is structurally narrower than a script's own NodeBody (a <<set>>/<<if>>/<<random>> segment can never legitimately survive execution; only text and resolved ActionCommand/InputCommand segments can), and dlb-core-java had no type distinguishing the two (#305, part of #210's "immutable runtime model" cleanup). Highlights:
    • NodeBody/Reply construction moves to NodeBody.Builder/Reply.Builder — addSegment(), addReply(), addCommand(), setStatement(), setNodePointer(), setReplyId(), setCommands(), and clearSegments() are all removed from the built types.
    • NodeBody.execute(Map, boolean, NodeBody) (void, writing into a caller-supplied body) is replaced by NodeBody.execute(Map, boolean), returning a new ResolvedNodeBody.
    • Command.executeBodyCommand(Map, NodeBody) is now executeBodyCommand(Map, ResolvedNodeBody.Builder) — affects every command subclass (SetCommand, ActionCommand, IfCommand, RandomCommand, the InputCommand family).
    • Node.getBody() and Reply.getStatement() are now typed against a new shared NodeContent interface, since each legitimately holds a NodeBody before execution and a ResolvedNodeBody after — code that already knows which one it has should cast.
    • Node itself is immutable now too (setHeader()/setBody()/the no-arg constructor are gone) — a direct consequence of NodeBody.execute() no longer needing a mutable body to write into.
    • Translatable no longer carries a parent field — it was used only to splice a translation back into a mutable tree in place, which no longer happens (Translator rebuilds instead).
  • Breaking: Core: NodeHeader is now immutable — setTitle(), setSpeaker(), setOptionalTags(), and addOptionalTag() are removed, getOptionalTags() now returns an unmodifiable view, and the no-arg constructor is gone in favor of NodeHeader(title), NodeHeader(title, speaker), NodeHeader(title, optionalTags), and NodeHeader(title, speaker, optionalTags) (#304, part of #210's "immutable runtime model" cleanup). Every field is now set at construction and never changes afterward, matching how Dialogue's own getters already defensively wrap their results — NodeHeader was the one place that claim didn't actually hold.
  • Core: parsing a node's body (BodyParser/CommandParser/ReplyParser) now reports every error found within it instead of stopping at the first (#211) — a node with an unrecognized command, a malformed reply, and another bad command further down previously surfaced only the first of the three; all three are now reported together. A node with any error is still excluded from the dialogue exactly as before — this only changes how much gets reported when something's wrong, not whether it counts as broken.
  • Breaking: Core: ActionCommand's type is now the ActionType enum (IMAGE/VIDEO/LINK/GENERIC) instead of a plain String — its constructor, getType(), and setType() all changed signature accordingly, and the TYPE_IMAGE/TYPE_VIDEO/TYPE_LINK/TYPE_GENERIC/VALID_TYPES constants are removed (use ActionType.IMAGE etc., or ActionType.fromWireValue(String)). Previously the "must be one of four known values" invariant was enforced only by the static parse() factory method, via a List<String>.contains() check external to the class — the public constructor and setType() didn't validate at all, so constructing one directly with an arbitrary string bypassed it entirely. The wire protocol (.dlb script type="..." attributes, and the type field the web service sends clients) is unaffected — both still use the same lowercase strings as before.
  • Studio: the Variable Browser's value tooltip now shows a variable's last-updated time in the time zone of the person using Studio (with the zone named), like the new History mode, rather than in the zone it was recorded in (#323).

Fixed ​

  • Web service: fixed a ClassCastException that broke every dialogue interaction past the first node. UserService.getDialogueState() reconstructs an ActiveDialogue per request (the API is stateless between calls) and restores its current node straight from the raw dialogue definition — script-stage content, not yet executed. ActiveDialogue.getUserStatementFromReplyId()/ processReplyAndGetNodePointer() assumed that node was always already-executed content and cast it accordingly, which threw on every restored session. Breaking: fixed by adding findReplyById(int) to the shared NodeContent interface (NodeBody/ResolvedNodeBody already implemented it identically; only the interface declaration was missing) — reply IDs are assigned once at parse time and never reassigned, so the lookup is valid against either script-stage or resolved content, removing the need to know which one a Node is currently holding.

[0.2.1] - 2026-09-23 ​

Added ​

  • Client JS: @dialoguebranch/client-js is now published to npm (#230) — a new release: published-triggered workflow runs its tests and npm publishes it on every GitHub release, keeping the published version in lockstep with the monorepo-wide version. Previously "private": true, consumed only via a file: dependency from within this monorepo.

[0.2.0] - 2026-09-23 ​

Changed ​

  • Breaking: Client JS: DialogueBranchAuthoringClient.listUsers now takes a single options object ({ usernameFragment, page, pageSize }) instead of a positional usernameFragment string, and exposes the Web Service's existing page/pageSize pagination on /users — previously always fetched page 0 only (undocumented beyond a JSDoc note), so a deployment with more than 50 known users couldn't reach the rest through Studio's "run as this user" picker.

Fixed ​

  • Client JS: packages/client-js/package.json's version now stays in sync with the monorepo-wide version in global.json. infrastructure/release/release-github.sh and its GitHub Actions equivalent (release-prepare.yml) both ran npm run sync-version for apps/studio but never for packages/client-js, even though it has the identical script — its package.json had drifted two patch releases stale (0.1.8 while global.json had moved on to 0.1.10). Harmless while the package stays unpublished, but would have shipped a wrong version to npm once it is. A new packaging test now asserts the two stay in sync.

[0.1.10] - 2026-09-23 ​

Fixed ​

  • Studio: the apps/studio Docker image builds again. Its Dockerfile didn't copy packages/client-js into the build context, so the file: dependency on @dialoguebranch/client-js couldn't be resolved and vite build failed.

[0.1.9] - 2026-09-23 ​

Fixed ​

  • BFF: apps/bff's /api/** proxy no longer double-encodes the forwarded query string. ApiProxyController forwards the incoming request's raw, already-percent-encoded query string verbatim, but RestClient's default URI-building encoded it a second time — an already-encoded value like timeZone=Europe%2FLisbon had its literal % re-encoded to %25, corrupting it into Europe%252FLisbon by the time it reached the Web Service, which then rejected it as invalid. Affected any Studio request whose query string needed escaping (a project slug or variable name/value with a space, &, /, etc.), not just timeZone — found via real browser testing while verifying #252, since timeZone had never actually been percent-encoded before that change and nothing else routinely triggered the double-encoding.
  • Client JS: ConsoleLogger now sends each log level to its corresponding console method (console.error/warn/info/debug) instead of always console.log, so browser/Node console severity filtering and error stack capture work as expected (#247).
  • Client JS: correct small JSDoc/README inaccuracies (constrcutor typo, Statement/AbstractLogger param types, `/whoami` markup, segments getter wording) and add missing class/constructor docs on Statement and DialogueStep (#248).

Removed ​

  • Breaking: Core: retired the pre-flattening multi-source-language handling in dlb-core-java (#109). Since a project has exactly one source language, the code path for a dialogue name resolving to several .dlb scripts (one per language folder) was dead except on a malformed project. Removed: execution.parser.DirectoryScriptLoader (its metadata-less "every sub-folder is a language" scan is exactly the retired model — use ProjectScriptLoader with a dlb-project.xml), the unused execution.parser.ResourceScriptLoader, and i18n.LanguageFinder (its only callers were the multi-variant source-language fallback). ProjectParser / ExecutableProject now do a single by-name lookup for a dialogue's source. (#87): the com.dialoguebranch.model.edit package (Editable, EditableBody, EditableHeader, EditableNode, EditableProject, EditableScript, EditableTranslation, EditableTranslationSet), the com.dialoguebranch.editing.parser package (EditableBodyParser, EditableHeaderParser, EditableProjectParser, EditableScriptParser, EditableTranslationParser), editing.writer.EditableScriptWriter and EditableTranslationWriter, editing.warning.ParserWarning, and cli.CommandLineRunner. This was the model layer for a Java-based dialogue-editing UI toolkit that was never built and was never functionally complete; editing now happens in Dialogue Branch Studio against the Web Service's own draft model. editing.writer.ProjectMetaDataWriter and cli.DialogueBranchCLI (the module's actual CLI, now repointed onto the runtime model) are unaffected. Breaking, but the project is pre-1.0 (0.x), so this ships in the next minor version rather than waiting for a major.
  • Breaking: Removed further stale CLI/vendor tooling from dlb-core-java (#94): i18n.POEditorTools (interactive import/export tooling tied to the POEditor SaaS, unused for years and with no tests), and execution.parser.DialogueBranchParser's main()/showUsage() entry point (a single-file parse CLI made redundant by cli.DialogueBranchCLI; DialogueBranchParser itself is unaffected). Also removed i18n.TranslationFile.writeToTSVFile(), orphaned by #87's removal of its only caller. Ships in the next minor version, alongside #87 (pre-1.0, see above).
  • Breaking: Removed further dead code from dlb-core-java, found in a follow-up scan after #87/#94: exception.ScriptParseException, exception.FileSystemException, and exception.VariableException (each used only by the deleted editable-model layer), model.common.ScriptTreeNode (used only by the deleted editable-model layer and the CLI code already repointed off it in #87), i18n.TranslationTerm (used only by the deleted POEditorTools), and two pre-existing orphans unrelated to either prior removal: model.common.DatabaseStorageSource (a StorageSource implementation never instantiated anywhere — the Web Service's DB-backed draft storage uses its own separate entities instead) and model.execute.DialogueStatus (an unused enum, never wired into DialogueState). Ships in the next minor version, alongside #87/#94 (pre-1.0, see above).

Added ​

  • BFF: apps/bff now exposes a generic GET /login end-point (LoginController) (#261). Studio's login trigger used to navigate straight to /oauth2/authorization/keycloak, this service's own fixed registration id, an internal detail a differently-structured BFF (one resolving a registration per request rather than having exactly one fixed client) has no way to predict. apps/studio/src/auth.js now navigates to /login instead, and any BFF fronting Studio can satisfy that same generic path however its own registration scheme works. Purely additive: /oauth2/authorization/keycloak itself is unchanged and still works.
  • New packages/client-js: a JavaScript client for a Dialogue Branch Web Service, @dialoguebranch/client-js (#88), extracted from what used to be Dialogue Branch Studio's in-app client library. Two classes: DialogueBranchClient (the default export) for running dialogues against a Web Service, and DialogueBranchAuthoringClient (./authoring) for project/dialogue authoring (#231) — a playback-only consumer (e.g. a future public demonstrator front end) only ever needs the former. No framework dependency, so it can be used outside Studio. Published as plain ESM with no build step. Not yet published to npm; Dialogue Branch Studio consumes it locally for now via a file: dependency. Comes with a README.md (install, a playback quickstart, the full list of exports subpaths) and full JSDoc on both classes' public methods (#233).
  • Web Service: an External Variable Service (EVS) may now report which variables it supports for a project, via an optional GET /v{version}/variables/supported?projectSlug=<slug> end-point on the EVS (#185). The Web Service exposes this to clients as GET /variables/list-supported?projectSlug=<slug> (editor/admin role, same as /variables/list-project), proxying each call to the EVS live with no caching. This is purely a discovery mechanism for authoring tools (e.g. confirming a $variable referenced in a dialogue is actually one the configured EVS knows about) — the Web Service does not validate, warn, or change execution behavior based on it, and dialogue execution remains fault-tolerant as before (a variable with no value is not an error). An EVS that does not implement the new end-point is unaffected; there is no API-version bump. apps/mock-variable-service implements the new end-point too, reporting the same three variables retrieve-updates already knows about (currentDate, currentTime, dayPart), regardless of the requested projectSlug. Dialogue Branch Studio's Variable Browser "Used" view now merges this into the existing list of variables referenced by the project's dialogues: a variable the configured EVS supports but no dialogue references yet still gets a row, and any row the EVS reports gets a small badge. Best-effort — a deployment with no EVS configured (the common case) sees no change from today's behavior.
  • Core: dlb-core-java's public API now carries a machine-readable nullness contract (#95). Every package is JSpecify @NullMarked (via a new package-info.java per package, which also adds the previously-missing package-level Javadoc), so unannotated reference types are non-null by default and the genuinely-nullable getters, setters, fields and parameters are marked @Nullable — e.g. Reply.getStatement(), ProjectMetaData.getSlug(), VariableStore.getVariable(), ProjectParserResult.getProject(). Adds an org.jspecify:jspecify dependency (annotations only, no runtime cost). Not a binary-compatible break, but downstream builds with strict null analysis may surface new warnings.
  • Core: the dlb-core-java nullness contract is now enforced in the build (#117). NullAway runs over com.dialoguebranch.* on every compile (a compile-only error-prone check, no runtime or published-classpath impact) and a violation fails the build. Getting there refined roughly 150 further nullness spots the #95 Javadoc pass had deferred — @Nullable on genuinely-optional values (Variable.getValue(), AttributesCommand.read*Attr(...), NodeParseException.getNodeTitle(), the i18n speaker/addressee parameters, …) and explicit guards where a value is contractually present — so the contract is both more complete and machine-checked. Downstream builds with strict null analysis may surface correspondingly more warnings.
  • Core: the expression engine vendored from rrd-utils in #102 (com.dialoguebranch.expression / .expression.types / .io / .util) is now under the same @NullMarked + NullAway contract as the rest of dlb-core-java (#121) — it had been excluded from the check. Its genuinely-nullable public surface is now @Nullable: Value.getValue() and the Value(...) constructor, Token.getValue(), Expression.evaluate(...)'s variables argument, ExpressionParser.readExpression() / readOperand(), and CurrentIterator.getCurrent(). Evaluation behaviour is unchanged. The two intricate state-machine classes (Tokenizer, ExpressionParser) and Value / LineColumnNumberReader keep a class-level NullAway suppression for their internal dataflow; documenting the engine's members is still open on #121.
  • Core: ProjectParser now detects orphaned nodes — a node that no reply link (internal or external) points to, and that isn't its own dialogue's Start node (#105). This can never cause a runtime error by design (a dialogue isn't required to link every node it defines), so it's reported as a warning via the existing ProjectParserResult.getWarnings(), already visible in DialogueBranchCLI's project summary.
  • Core: cli.DialogueBranchCLI (renamed from ProjectTool — see below) now supports non-interactive, scriptable invocation alongside its existing interactive menu (#105): DialogueBranchCLI <path-to-dlb-project.xml> [--validate] parses a project and prints its summary, exiting non-zero on parse errors (warnings, such as orphaned nodes, do not affect the exit status) — usable as a CI gate. DialogueBranchCLI <path> --execute <language> <dialogue> runs a specific dialogue, reusing the same interactive terminal conversation as before. The no-argument interactive session is unchanged and remains the default.
  • Permission-based access control for the Web Service (#28): a Permission catalogue, a Role → permission mapping (participant ⊂ editor ⊂ admin), and an AuthorizationService that decides whether a caller may perform an operation.
  • Studio: <<action>> commands now render in the dialogue view (#178). An action attached to an agent statement is shown inline — link as an anchor, image as an <img>, video as a player for a direct media URL (a labelled link otherwise), and generic as a chip showing its value. Actions attached to a reply are logged to the Debug Console on selection; the participant view also emits a dlb-action CustomEvent on window for an embedding page to react to. Previously every non-text statement segment was dropped from the rendered dialogue.
  • Studio: <<input>> replies now work in the dialogue view (#177). A reply containing an <<input>> command renders the matching widget inline (single-line / multi-line text, email, number, time picker, or a checkbox group for set), validates it against the command's parameters, and submits the collected value(s) to the Web Service as the /dialogue/progress (and /draft/progress) request body — which previously was never sent, so input replies could not be used at all. Applies to both the authoring test view and the participant view.
  • Web Service + Studio: pick a delegate user by name instead of typing a raw subject (#130). New GET /v1/users endpoint — a read-only, realm-scoped listing of the users this service has run a dialogue for ({ username, subject } entries, optional username substring filter, paginated), gated on a new admin-only USER_LIST permission. Studio's "Set Delegate User" dialog is now a typeahead against that endpoint: the admin types a username, picks from the results, and Studio sends the resolved subject to the dialogue endpoints; a raw-subject field remains for users not yet known to the service.

Changed ​

  • Breaking: Client JS: DialogueBranchAuthoringClient's startDraftDialogue, progressDraftDialogue, and revertDraftVariables now take a single options object instead of positional arguments, and no longer silently infer timeZone — it's sent only when explicitly given, the same treatment #252 gave DialogueBranchClient (#256). BaseClient's Intl.DateTimeFormat()-computed _timeZone field is removed entirely — nothing reads it any more, on either client class.
  • Breaking: Client JS: DialogueBranchClient's listDialogues, startDialogue, continueDialogue, getVariables, getOngoingDialogue, and setVariable now take a single options object instead of positional arguments, and no longer require (or silently infer) projectSlug/language/timeZone — each is sent only when explicitly given (#252). Previously these were positional and timeZone was always auto-computed from Intl.DateTimeFormat(); a deployment behind a backend that resolves the project/language/time zone itself (from the caller's own account or session) can now omit them entirely rather than having the client send values the backend will ignore. A direct Dialogue Branch Web Service consumer must now pass timeZone explicitly — the client never infers it. Also adds DialogueBranchClient.back(loggedDialogueId, loggedInteractionIndex), wrapping the previously-unexposed POST /dialogue/back endpoint.
  • Breaking: Client JS: every wire-protocol model type now has a fromJSON parser (previously only Action/Segment did, despite protocol.js claiming otherwise) (#232). getServerInfo() and getOngoingDialogue() now return ServerInfo/OngoingDialogue instances instead of raw parsed JSON, matching every other method that already returned a typed model. Also fixed two model classes that had drifted from the Web Service's actual response shape: OngoingDialogue was missing loggedDialogueId entirely (and had name where the wire uses dialogueName) — harmless before since nothing constructed one, but would have silently dropped the field Studio's own "resume dialogue" prompt needs; Variable didn't formally model updatedSource even though it's part of the real response and Studio already displays it. Breaking for anyone relying on the old plain-object return shape or the pre-fix OngoingDialogue fields, but nothing outside this monorepo consumes the package yet.
  • Breaking: Client JS: DialogueBranchClient and DialogueBranchAuthoringClient now reject failed requests with a DialogueBranchError (a real Error subclass — instanceof Error and .stack now work; status/statusText/code/fieldErrors/errors are still there as properties) instead of a plain object (#234). error.message keeps working unchanged. Breaking for anyone destructuring the old plain-object shape, but nothing outside this monorepo consumes it yet.
  • The bundled default-test seed project (and the mirrored examples/project-test) has been reworked into a guided tour that exercises every .dlb language feature (#173): new lesson dialogues for statements/escaping, random, action (all four types), and input (all six types), plus expanded variables (arithmetic, string coercion) and conditionals (elseif, comparison and boolean operators) lessons, all linked from menu.dlb and fully translated into nl-NL and pt-PT. Project version bumped to 2. A new packages/core FeatureCoverageTest fails if the project stops demonstrating any construct.
  • Studio: the Variable Browser "Values" tab now shows only the currently selected project's variables, not a cross-project dump (#86). The client's getVariables / setVariable calls pass the project slug; with no project selected the tab is empty.
  • Breaking: External Variable Service: the retrieve-updates, notify-updated and notify-cleared requests now identify the user and project with subject + issuer + projectSlug instead of a single bare userId (#128 + #86). subject is the stable OIDC sub, issuer the realm URL (needed because one instance may trust several realms), and projectSlug scopes the variables. No DLB_EXTERNAL_VARIABLE_SERVICE_API_VERSION bump — the bundled apps/mock-variable-service and the docs are updated in lockstep; a real external service must be updated to match.
  • Breaking: Web Service: Dialogue Branch Variables are now scoped to the project (#86). Projects on one Web Service instance are isolated tenants, so a user who runs dialogues in two projects no longer shares one variable namespace. Variables are keyed (user, project, name): variables gains a project_id (NOT NULL, FK → projects), the UNIQUE(user_id, name) constraint becomes UNIQUE(user_id, project_id, name), and each user's runtime VariableStore is now per-project. GET /v1/variables/get, POST /v1/variables/set-single and POST /v1/variables/set gain a required projectSlug parameter. Flyway migration V6 adds the column and drops all existing variables rows (there is no per-project attribution to recover — dev-only data, no deployment on this schema). packages/core is unchanged. The External Variable Service request contract and the Studio Variable Browser are updated in the following steps of this change.
  • Breaking: Web Service: a Dialogue Branch user is now identified by the JWT's (issuer, subject) pair instead of preferred_username (#128). preferred_username is not realm-qualified (one instance trusts tokens from multiple Keycloak realms) and is mutable/reusable, so two people could collapse onto one variable store + dialogue history, or have their data orphaned on a rename. The users table gains issuer and subject columns (UNIQUE(issuer, subject)); username becomes a mutable display attribute, refreshed from the token on each login. The delegateUser request parameter (/dialogue/*, /variables/*, /log/*) now takes the delegate's OIDC subject within the caller's realm, not a bare username — there is deliberately no cross-realm delegation. Flyway migration V5 adds the columns and, since sub was never stored, wipes the existing user-scoped tables (users, variables, logged_dialogues) — dev-only data, no deployment exists on this schema. The External Variable Service request contract is unchanged in this step; it gains issuer + subject together with the project slug from #86 in a single following revision.
  • Core: execution.parser.ProjectScriptLoader now validates a project's on-disk layout when it is constructed (#109): the metadata must declare a source language, the source-language folder may hold only .dlb files, and each translation-language folder may hold only .json files (checked recursively; dot-files and non-language entries at the project root are ignored). A violation is a ParseException. ProjectParser additionally reports a dialogue name defined by more than one script file as a parse error, regardless of the ScriptLoader in use. Both were previously accepted silently.
  • Renamed cli.ProjectTool to cli.DialogueBranchCLI (#105) — the old name dated back to when it was one of several overlapping CLI entry points (see #87/#94); now that it's the library's sole CLI and does more than inspect projects (it also validates and executes dialogues), the name should say so. build.gradle's mainClass and the jar manifest's Main-Class are updated to match.
  • Endpoint authorization is now expressed as one required Permission per end-point (resolved from the caller's roles via the central role→permission map) instead of an inline list of accepted roles (#58). Which roles may call each end-point is unchanged, but an authenticated caller who lacks the required permission — including a non-admin who passes delegateUser — now gets 403 Forbidden (previously 401 Unauthorized), still with error code INSUFFICIENT_PRIVILEGES; a missing, expired, or invalid token remains 401.
  • Studio's "you can't do this" tooltips on gated actions (Publish / Configure / Export project, Import / New project) no longer name a specific role — they now read "You don't have permission to …", so they stay correct if the role a capability requires ever changes.
  • Core: replaced rrd-utils' I18nLanguageFinder with a small native com.dialoguebranch.i18n.LanguageFinder built on java.util.Locale RFC 4647 lookup, as the first step of dropping the nl.rrd:rrd-utils dependency from dlb-core-java (#102). (Subsequently removed again in #109 — its only callers were the now-retired multi-source-language fallback.)
  • Breaking: Core: vendored the .dlb <<if>> / <<set>> expression engine into com.dialoguebranch.expression (+ .types), and moved the two parser exceptions it shares with the rest of the library — ParseException and LineNumberParseException — into com.dialoguebranch.exception (folded under DialogueBranchException), continuing #102's removal of the nl.rrd:rrd-utils dependency. Evaluation semantics, .dlb syntax and the wire format are unchanged, but a direct dlb-core-java consumer that referenced nl.rrd.utils.expressions.* types, or caught nl.rrd.utils.exception.ParseException / LineNumberParseException, must switch to the com.dialoguebranch.* equivalents. Project metadata (dlb-project.xml) is now read with the JDK's StAX parser instead of rrd-utils' SAX helper, with DTD / external-entity resolution disabled.
  • Breaking: Core: dropped the nl.rrd.utils xml, io.FileUtils and json.JsonObject usages from dlb-core-java (continuing #102). editing.writer.ProjectMetaDataWriter now writes dlb-project.xml with the JDK's DOM / Transformer APIs and its entry point changed from writeToXMLFile(XMLWriter, ProjectMetaData) to writeToXMLFile(OutputStream, ProjectMetaData) (the caller no longer constructs, or needs, an nl.rrd.utils.xml.XMLWriter); model.execute.protocol.NullableResponse no longer extends nl.rrd.utils.json.JsonObject (its toString() output changes, and it no longer has map-based equals / hashCode). Output formatting of the exported metadata file changes slightly (indentation, XML declaration); it still round-trips through the reader unchanged.
  • Breaking: dlb-core-java no longer depends on nl.rrd:rrd-utils (#102 — the goal of that issue). The last three uses were vendored or replaced: CurrentIterator moved to com.dialoguebranch.util, ReferenceParameter in the body tokenizer became a plain int[] out-parameter, and BodyToken.toString() no longer routes through DataFormatter. rrd-utils is dropped from the published POM — a consumer that was relying on getting it transitively from dlb-core-java must now declare it directly.
  • The Web Service (apps/api) and the External Variable Service (apps/mock-variable-service) no longer depend on nl.rrd:rrd-utils either (#136) — the platform now has no rrd-utils dependency anywhere. DatabaseException moves to the existing com.dialoguebranch.web.service.exception.DatabaseException; DateTimeUtils.nowMs(zone) becomes a two-line local helper over ZonedDateTime.now(zone).truncatedTo(MILLIS); the request-body reads use InputStream.readAllBytes; the base-path parse uses java.net.URI; the forbidden-query-param check parses the query string directly; and the HttpError / HttpFieldError / DialogueListPayload DTOs drop extends JsonObject for a plain toString(). No REST payload, protocol, or behaviour change.
  • Core: execution.parser.ParserResult and ProjectParserResult are now constructed with their required value (the parsed Dialogue / the ScriptLoader) instead of a no-arg constructor plus a setter, and ExpressionCommand's internal ReadContentResult / ParseContentResult likewise (#121) — the fields are now final and the @SuppressWarnings("NullAway.Init") markers are gone. ParserResult() / ParserResult.setDialogue(...) and ProjectParserResult() / setScriptLoader(...) are removed; both types are only ever handed back by the parser, never constructed by callers.
  • Core: execution.ActiveDialogue now takes its VariableStore as a third constructor argument (#121) — previously the field was set by a mandatory setVariableStore(...) call right after construction. The two-argument constructor is removed. setVariableStore(...) is kept for the one case that genuinely re-points the store (re-attaching a persisted DialogueState to a fresh user session).
  • Core: internal parser cleanup (#121) — execution.parser.DialogueBranchParser sets its dialogueName / reader in a canonical private constructor (the public constructors are unchanged), and BodyParser.ParseUntilCommandClauseResult takes its body as a constructor argument. No behaviour change.
  • Core: execution.parser.BodyToken is now built via a constructor (BodyToken(Type, String text, int lineNumber, int colNumber[, Object value])) rather than a no-arg constructor plus setters (#121); setType / setText / setLineNumber / setColNumber are removed and its fields are final. getValue() is now @Nullable (it is null for the token types that carry no value). ReplyParser threads its statement / node-pointer / command sections through as method arguments instead of scratch fields. Tokenisation and reply parsing are unchanged.
  • Breaking: Core: model.execute.protocol.DialogueAction is now built via an all-args @JsonCreator constructor (type, value, optional parameters) with final fields, instead of a no-arg constructor plus setters (#121). DialogueAction(), setType(...), setValue(...) and setParameters(...) are removed. The JSON wire format — {"type":…,"value":…,"parameters":{…}} — is unchanged and still round-trips (a JSON document with no parameters field deserializes to an empty map).
  • Breaking: Core: model.execute.protocol.DialogueMessage likewise moves to an all-args @JsonCreator constructor with final fields (#121); its no-arg constructor, seven setters and addReply(...) are removed and DialogueMessageFactory builds the message in one step. The /dialogue/* response JSON is unchanged.
  • Breaking: Core: the three model.execute.protocol.DialogueStatement segment classes — TextSegment, InputSegment, ActionSegment — are now built via constructors (TextSegment(String) / ActionSegment(DialogueAction) are @JsonCreators; InputSegment's is called by its custom deserializer) with final fields (#121); their no-arg constructors and setText / setInputType / setDescription / setParameters / setAction are removed. The statement / segment JSON — including InputSegment's flattened parameter form — is unchanged.
  • Breaking: Core: model.common.ProjectMetaData is now built via a ProjectMetaData(name, description, version) constructor with final name / description / version (#121); the no-arg and five/six-arg constructors and setName / setDescription / setVersion are removed, and getStorageSource() is now @Nullable (only metadata read from a store carries one). The XML parser now yields "" rather than null from getDescription() for a project whose metadata has no <description> element.

Fixed ​

  • Web Service: GET /variables/list-supported now returns 501 when no External Variable Service is configured and 503 when the configured service is unavailable, instead of reporting both conditions as 500 (#243).
  • Client JS: URL-encode string query parameters used by dialogue playback and draft-test requests so reserved characters do not corrupt the request URL (#246).
  • Client JS: limit the npm package to runtime modules, include its MIT licence, and add repository and issue-tracker metadata (#236).
  • Web Service: role extraction now honours dlb.auth.keycloak.client-id (#104). A deployment that set a non-default client id received tokens whose roles live under resource_access.<that-id>.roles, but role extraction always looked under a hardcoded dlb-web-service key — so every authenticated user resolved to no roles and got 403 INSUFFICIENT_PRIVILEGES on every permission-gated endpoint. azp validation already respected the setting; the two now agree. The "user has no roles" warning also names the configured client id and lists the resource_access keys the token did carry.
  • Core: updated the interactive banner and entry-point Javadoc to use "Dialogue Branch CLI" instead of the old "Dialogue Branch Project Tool" name (#110).
  • Studio: the "Continue" button in the balloon test view (shown for auto-forward dialogue steps) was near-invisible — a transparent fill with a border and text colour that matched the grey page background. It now has a light fill, an orange border and dark text, so it reads clearly for participants and editors alike (#98).

Security ​

  • The Web Service now checks each token's azp (client) claim against dlb.auth.keycloak.trusted-clients (env DLB_AUTH_KEYCLOAK_TRUSTED_CLIENTS), on top of the existing signature/issuer/expiry checks. Unset, it trusts its own client-id and the BFF's (dlb-bff), which covers the standard topology (direct API clients / Swagger UI, and Dialogue Branch Studio via the BFF) with no configuration. Set the property to a comma-separated list to trust additional clients in a shared realm, or to a single * to accept any client from a trusted realm (the previous behaviour); an explicit list replaces the default entirely, so it must still name dlb-bff on a BFF-fronted deployment. Tokens from an untrusted client are rejected with 401 (#67).

[0.1.8] - 2026-09-01 ​

Changed ​

  • Dialogue Browser entries (folders and dialogues alike) now render as grey row blocks, matching the Variable Browser, and each block is inset by its nesting depth.
  • The web client is now called "Dialogue Branch Studio" in its own UI: the footer info bar, the browser tab title, the project-selector subtitle, and the version-mismatch page.
  • Studio's balloon test view steps the speech-bubble font size down for long statements (to text-base past ~400 characters, text-sm past ~900) so more of a long node fits before the reader has to scroll the page. Within one dialogue run the size only ever shrinks — a shorter node after a long one keeps the reduced size instead of bouncing back up.
  • Breaking: POST /v1/project/create-project now expects the source-language fields as sourceLanguageCode / sourceLanguageName instead of defaultLanguageCode / defaultLanguageName. The old names are no longer accepted (a request using them fails with "Field 'sourceLanguageCode' is required."). This aligns the create request with the rest of the API, which already uses sourceLanguage* everywhere else (the create response, GET /v1/project/get-project, and the database). The bundled Dialogue Branch Studio is updated to match.

Added ​

  • Core: Reply.isAutoForward() — names the "reply without a statement" check; the conceptual call sites in the parser, executor, message factory, translatable extractor and CLI now use it instead of open-coding getStatement() == null.
  • apps/studio now has a unit-test setup — Vitest + Vue Test Utils + jsdom, run with npm test. Initial coverage is the dialogue-testing views (reply-option highlighting, the per-tab scroll API, statement paragraph rendering, and the speech-bubble font-size ratchet) and the Dialogue Browser sort/filter helpers.
  • The Variable Browser has a Values / Used toggle. Values is unchanged — the logged-in user's stored variable values. Used is a new read-only reference list of every variable name the project's dialogues reference, so you can look up the exact name of a variable ($userName vs $user_name …) without having triggered it first; hover a row to copy its name. Names the user already has a value for show solid; ones with no value yet are slightly dimmed (#64).
  • New end-point GET /v1/variables/list-project?projectSlug=… (editor/admin) backing the above: returns every variable any of a project's dialogues reads or writes, sorted by name and flagged read/written, regardless of whether a value is stored for it.
  • The Dialogue Browser has a filter + sort strip below its header. The filter box narrows the tree to matching dialogues/folders (folders auto-expand while filtering, without disturbing the saved expand state). The sort control offers Default (folders first, then alphabetical — the API's own order) and Name (A–Z / Z–A) (a flat sort with folders and dialogues intermixed) in both modes, plus Last updated and Size (by node count, folders kept grouped first) in Authoring Mode.
  • The Variable Browser has the same filter + sort strip: a name filter and Name (A–Z / Z–A) ordering (variables now default to alphabetical order rather than the order the service returns them in).
  • The authoring list-dialogues response now includes each draft dialogue's updatedAt and nodeCount, and is ordered folders-first, so a client can offer sort-by-last-updated and sort-by-size in Authoring Mode. Node counts come from a single grouped query (no node content is loaded).
  • The debug bar at the bottom of Studio's dialogue test views now also shows the node the current step is in, next to the ephemeral session / logged-dialogue id.
  • Studio's admin-only "Technical Information" dialog now also shows the Web Service's version and build time, its configured base URL (dlb.base-url), the host/port/scheme it observed on the incoming request (as seen by the service itself — behind the BFF proxy this is the address the proxy connected to, not a browser-facing URL), and the Keycloak base URL and realm it validates tokens against. These come from new fields on the GET /v1/info/technical response, which remains restricted to the admin role.

Fixed ​

  • Parsing a project's metadata (dlb-project.xml) now rejects a language-map whose source or translation languages are missing a code, or where two languages share one (case-insensitive), instead of silently building a broken language configuration.
  • list-dialogues now returns dialogue names ordered "folders first" — at each /-separated level, sub-folders come before loose dialogues, then alphabetical (case-insensitive) — instead of a plain lexicographic sort. Studio builds its own tree so its Dialogue Browser was already correct; this fixes the order seen by clients that render the API response directly.
  • Text-mode dialogue testing in Studio no longer shows a fresh dialogue's reply options greyed out (or with one option falsely highlighted as already chosen). The component tracked past reply selections by step index without clearing them when the dialogue was restarted, reloaded, or when switching to another test tab, so stale entries carried over onto the new content.
  • Text-mode test tabs now keep their own scroll position. The history view's scroll container and spacer height were shared by every tab, so switching tabs left the view at whatever offset the previous tab had. Each tab's offset is now saved on the way out and restored on the way back in (a tab shown for the first time still pins to the latest statement).
  • Dialogue statement text with paragraph breaks now renders as separate paragraphs in Studio's balloon and text test views instead of collapsing into one block. Blank lines in a node's body become <p> breaks and single line breaks become <br>; the newlines were already preserved by the parser and web service, only Studio's HTML rendering dropped them.
  • Studio's status bar no longer reports a misleading "Connected to <host> on port <port>" line. Since Studio talks only to its own origin and the Web Service sits behind the BFF proxy, that host and port were Studio's own, not the Web Service's. It now reads simply "Connected to Web Service v<version>." (the version is still the real one, fetched through the proxy), or "Could not connect to the Web Service." on failure.
  • The "Create Project" wizard's third step now calls the setting the project's "Source Language" rather than "Default Language", and explains that it's the language dialogue content is authored in and that other languages are added later as translations (#76). This matches the wording already used in the project configuration dialog.

[0.1.7] - 2026-08-12 ​

Added ​

  • The visual dialogue editor's node graph now supports right-clicking empty canvas to create a new node at that location, in addition to the existing "Add Node" toolbar button.
  • The visual dialogue editor's node body field now has a right-click menu for inserting Dialogue Branch statements (reply options, <<set>>, <<if>>/<<elseif>>/<<else>>, <<random>>, <<action>>, and all <<input>> types) at the cursor, instead of writing the raw syntax by hand.
  • The "Service Unavailable" screen's status icons now show the exact URL each reachability check hit, on hover, to make it easier to tell which backend a failure is actually pointing at.
  • The Web Service's resource server now trusts more than one Keycloak realm: the configured dlb.auth.keycloak.realm plus any realm on the same Keycloak instance named <that realm>-<anything>. This supports a hosting platform that provisions one Keycloak realm per client or tenant on top of its own base realm, all sharing this same Dialogue Branch backend and database; previously the JWT filter validated exclusively against the single configured realm, rejecting an otherwise valid token from any other realm outright. The realm name is only ever taken from a token's own (unverified) iss claim to select which known-trusted realm to check the token's signature against, not to pick an arbitrary network address to call out to.

[0.1.6] - 2026-07-29 ​

Added ​

  • Added a client Docker Compose profile (infrastructure/docker/compose.yml) that containerizes the web client and BFF behind a new nginx reverse proxy (infrastructure/docker/nginx/client.conf), mirroring a real deployment (#81). This gives a third local development setup, alongside the existing two: docker compose --profile client up -d runs everything (API, web client, BFF, proxy) in Docker on a single origin, http://localhost:8080, with no separate dev server needed.
  • The BFF (apps/bff) now persists sessions to MariaDB via Spring Session, instead of the JVM's own heap, so a redeploy no longer silently logs out every signed-in user. This includes the OAuth2AuthorizedClient holding the session's access/refresh token, which Spring Boot's default wiring keeps separately from the session itself and would otherwise still be lost on redeploy. The session inactivity timeout (also driving the SESSION cookie's Max-Age) is now configurable via dlb.bff.session-timeout (DLB_BFF_SESSION_TIMEOUT), defaulting to 7 days instead of Spring Boot's own 30-minute default. The cookie is re-issued on every request so it slides forward with activity instead of hard-expiring on a fixed clock from login.
  • infrastructure/keycloak/sync-realm.py now also reconciles redirect/post-logout URIs on Keycloak clients that already exist, not just clients that are entirely missing. Previously, a change like the client profile's new origin never reached a developer's already-provisioned local Keycloak without manually resetting the MariaDB volume.

Fixed ​

  • Fixed BFF logout failing with Keycloak's "Invalid redirect uri" for same-origin deployments (the new client profile, and production behind a reverse proxy). The BFF sent post_logout_redirect_uri to Keycloak as a literal relative /, which Keycloak's end_session_endpoint rejects since it requires an absolute, registered URI. This differs from the login-success redirect, which a browser resolves relative to its own current origin regardless. Now resolves via OidcClientInitiatedLogoutSuccessHandler's {baseUrl} placeholder when the redirect URL is left at its default, honoring the reverse proxy's forwarded headers.

[0.1.5] - 2026-07-29 ​

Added ​

  • Added a Backend-for-Frontend (BFF) service (apps/bff) between the web client and Keycloak/the Dialogue Branch Web Service (#79). The web client no longer talks to Keycloak or the API directly, and no longer holds an access token in the browser at all — the BFF performs the login exchange with Keycloak and keeps the session's token server-side, so the browser only ever holds a session cookie. All API calls now go through the BFF at /api/**, and a new /whoami endpoint returns the current session's username and roles. Logging out now also ends the Keycloak SSO session, not just the local one, so logging back in properly re-prompts for credentials instead of silently resuming the same session. Local development gained a matching bff Docker Compose service (started via the existing api profile) and a one-shot keycloak-sync service that adds the BFF's Keycloak client to a realm that already existed before this change.
  • Added a variable-service Docker Compose profile (infrastructure/docker/compose.yml) that builds and runs the mock external variable service alongside MariaDB and Keycloak, for testing dlb-web-service's external variable service integration locally.
  • The mock variable service now also resolves a $dayPart variable on retrieve-updates requests, returning "morning", "afternoon", "evening", or "night" based on the current hour in the user's time zone, alongside the existing $currentDate/$currentTime handling.
  • Added an "Insufficient Privileges" screen to the web client: a user who successfully logs in but lacks the required Dialogue Branch role (admin, editor, or participant) now sees an explanation and a Log Out button, instead of being silently logged out and redirected straight back to the login page with no indication of why.

Changed ​

  • The web client's production build now splits dlb-lib (the framework-agnostic client library for talking to a Dialogue Branch Web Service, intended to be reusable outside this app) into its own JS chunk instead of bundling it into the main app chunk, via a manualChunks rule in vite.config.js.
  • The web client now imports only the specific Font Awesome icons it actually uses, instead of the entire solid and regular icon sets. This shrinks the main production bundle from ~1.49 MB to ~450 kB (minified), removing the build's "chunks larger than 500 kB" warning entirely.

Fixed ​

  • Fixed the mock variable service's Docker image, which failed to build: it referenced source paths that no longer exist and packaged the app as a WAR for a standalone Tomcat instead of the Spring Boot jar it's built as today. Its logging config also referenced a missing resource file, which crashed the service on startup.
  • Fixed a NullPointerException thrown when starting a dialogue that references a variable with no existing value while an external variable service is enabled.

[0.1.4] - 2026-07-22 ​

Added ​

  • Added project export/import: a project's published content can be downloaded as a .zip archive ("Export Project" in the project menu, Live Mode only, once published) and re-created as a new project from that archive ("Import Project" from the project selector). New endpoints GET /project/export-project and POST /project/import-project. Import validates the archive (size caps, zip-slip protection, project-content validation, slug-collision check) before creating anything in the database. Export fetches a project's dialogues and translations in two batched queries instead of one query per dialogue. Importing an archive preserves its exported version number instead of always restarting the new project at version 1.
  • Added a service status check to the web client's startup: before redirecting to Keycloak's login page, it now verifies the DLB API and Keycloak are both reachable. If either is down, it shows a status page with a Retry button instead of sending the user to a broken login page. The same check also compares the API's reported software version against the web client's own build version; a mismatch (e.g. a stale cached bundle after a deploy) shows an "Update Available" page with a Reload button instead of proceeding to login.

Fixed ​

  • The web client footer version had drifted from global.json (still showing v2.0.1 after the v2.0.3 release) because apps/web/package.json was never re-synced. infrastructure/release/release-github.sh now runs npm run sync-version in apps/web right after bumping global.json, and includes apps/web/package.json/package-lock.json in the release commit, so this can't drift again.

[0.1.3] - 2026-07-20 ​

Added ​

  • Moved project metadata (display name, description, translation languages) into the same draft → publish cycle as dialogue content (#75). Editing a project's metadata used to write directly to the live/published record with no review step; a new "Configure Project" window (renamed from "Edit Metadata", and now only available in Authoring Mode) edits a draft copy instead, with a General tab (display name, description, read-only project slug, and the latest published version's info) and a Languages tab (the source language shown read-only, plus add/rename/remove for translation languages). Nothing is sent to the server until "Save Draft" is pressed, and the whole batch of pending changes — metadata plus any language removals, additions, and renames — is applied atomically through a new POST /project/update-draft end-point: either all of it succeeds, or none of it does, with structured per-field errors reported back for whichever change conflicted (e.g. two languages ending up with the same code). Removing a translation language warns which draft dialogues currently have content in it before it's removed, and — like dialogue deletion — is reversible (with an undo) until the project is next published. Draft and published translation content also gained real referential integrity: DBDraftTranslation/DBPublishedTranslation now reference their language via a foreign key into a language registry, rather than an unconstrained string column. Finally, publishing now snapshots a project's display name, description, and translation-language list onto the new, immutable published version — exactly like dialogue content already is — so a past published version retains its own historical metadata and language list instead of sharing one project-wide, mutable "current" record.
  • Replaced the Antora/AsciiDoc-based Documentation Hub with a new VitePress-based site (documentation/vitepress/), now live at https://www.dialoguebranch.com/docs/. The vendored antora-ui-default fork (documentation/dlb-ui/) had drifted from the rest of the platform's look (pinned to Node 10, a separate gulp/browserify toolchain to maintain) and contributed 48 open Dependabot alerts, all in its own package-lock.json, none reachable from the shipped product. The new site reuses the Web Client Test Application's brand palette and self-hosted "Roboto Slab" font, and is fronted by a header mirroring the rest of www.dialoguebranch.com (Home / Documentation / About / News & Updates) so the docs, marketing site, and web client read as one product. All content was ported over 1:1.

Fixed ​

  • Fixed "Publish Project" staying disabled in the web client's Authoring Mode when the only unpublished change was to translation content. Saving a translation cell in TranslationEditor .vue already marked the owning draft dialogue as changed server-side, but the component had no way to tell the rest of the app a save had happened — unlike the node editor (DialogueEditor .vue), which emits dialogueSaved after every edit, prompting DialogueBrowser.vue to refetch the draft dialogue list and recompute whether anything is unpublished. TranslationEditor.vue now emits the same dialogueSaved event after each successful translation save.

Removed ​

  • Removed the old Antora/AsciiDoc documentation project (documentation/antora/, including the vendored dlb-ui UI fork) from the repo now that the VitePress site above has fully replaced it in production.

[0.1.2] - 2026-07-17 ​

Added ​

  • Added support for cross-dialogue reply links ([[Reply Text.|otherDialogue.NodeId]]) in draft test sessions (/draft/*) — selecting such a reply used to fail with "This reply points to another dialogue, which isn't supported in draft test mode." (#74). DraftExecutionService .startSession already parsed the whole project (every draft dialogue, not just the one under test) precisely so sibling references would resolve, but that parsed ExecutableProject was a local variable, discarded once the session started, leaving progressSession with no way to actually follow a link when one turned up. DraftTestSession now keeps the parsed project (and the project's source language, needed to resolve languages the same way published dialogues do — not reliably available from the parsed project's own metadata, since these dialogues are parsed from in-memory content maps, not an actual dlb-project.xml) for the life of the session; progressSession resolves the target dialogue against it and switches the session over via a new DraftTestSession.switchToDialogue, mirroring how DialogueExecutor already handles the same case for published dialogues. No Web Client changes were needed — DialogueWorkspace .vue already updates a tab's displayed dialogue name from whatever the server's response says on every progress step.
  • Added an optional startNodeId parameter to the API's POST /dialogue/start end-point, to start a live (published) dialogue session at a specific node instead of always the default "Start" node — mirroring what /draft/start already supported for draft testing. UserService.startDialogueSession already accepted this; only the controller's @RequestParam plumbing was missing. Used by the Web Client: switching the "Test dialogues in:" language selector while a dialogue is actively running in the current tab now restarts that tab's test at its current node, in the newly selected language, instead of silently leaving it running in whichever language it happened to be started in (#69).
  • Added a small "x" icon to reply options in the Web Client's Speech Bubble and RPG Text test modes (BalloonDialogueComponent.vue/TextDialogueComponent.vue) when selecting that reply would end the dialogue, so a tester can see this before choosing it rather than being surprised afterward (#73). This was a pure front-end gap: the API already computed and sent this per-reply (ReplyMessage.endsDialogue, set by DialogueMessageFactory whenever a reply points directly at the dialogue's End node), and the client's Reply/BasicReply/AutoForwardReply models and DialogueBranchClient already parsed it — it just was never rendered anywhere.
  • Added a dedicated Web Client layout for participant-only users (no editor/admin), so the participant role can actually be tested end-to-end. Previously main.js's role gate excluded anyone without editor/admin entirely, logging them straight back out — and even if let in, a participant can't call listProjects/getProject/listDialogues (all editor/admin-only), so ProjectSelectorPage/MainPage would just fail to load. App.vue now routes a participant-only user (checked ahead of the normal routing, so an editor/admin who also carries participant still gets the full app) to a new ParticipantPage.vue, which auto-starts the configured project/dialogue (config.participant.projectSlug/dialogueName, defaulting to default-test/ menu; override via VITE_DLB_PARTICIPANT_PROJECT_SLUG/VITE_DLB_PARTICIPANT_DIALOGUE_NAME) through the existing BalloonDialogueComponent, using only endpoints a plain participant can actually call (/dialogue/start, /dialogue/progress). If the project or dialogue doesn't exist, the resulting 404 is shown inline (via BalloonDialogueComponent's existing startError/"Try Again" state) rather than as a toast, since there's nothing else on this single-purpose page for the toast to sit alongside.
  • Added an admin-only GET /info/technical end-point (TechnicalInfoPayload) returning technical information about the running service — currently the number of active (in-memory) UserService instances (ApplicationManager.getActiveUserServiceCount()). Unlike /info/all, this is not in SecurityConfig's permitAll() list, so it requires normal bearer-token auth plus the admin role. In the Web Client, admins now see a small "i" icon after the connection status line in the footer; clicking it opens a TechnicalInfoModal showing this information (DialogueBranchClient.getTechnicalInfo()).
  • Added an idle-timeout eviction sweep for in-memory UserServices. A UserService was otherwise only ever removed by an explicit /auth/logout call, so a client that disconnects without logging out (closed tab, killed app, expired token, dropped connection) left its UserService — and its VariableStore, LoggedDialogueStore — in memory for the remaining lifetime of the server process; since activeUserServices only grows and never shrinks on its own, a long-running server accumulates one per distinct user who has ever authenticated, not per user currently active. UserService now records a lastActivityTime, refreshed on every request that resolves an existing UserService (ApplicationManager.getActiveUserService); a new UserServiceExpirationService (@Scheduled, every 5 minutes) evicts any UserService idle longer than dlb.session.idle-timeout-minutes (DLB_SESSION_IDLE_TIMEOUT_MINUTES, default 60). ApplicationManager.activeUserServices moved from ArrayList to CopyOnWriteArrayList, since it's now mutated concurrently by request threads and the scheduled sweep, not just by request threads racing each other.

Changed ​

  • Gave the Web Client's auto-generated "Continue" reply buttons (AutoForwardReply — an autoforward with no [[Reply Text.|NextNode]] of its own) a distinct, secondary/outline style in both the Speech Bubble and RPG Text test modes, instead of the same solid orange button used for real, author-defined reply options (BasicReply) (#68). A tester can now tell at a glance which buttons represent an actual choice versus a mechanical "just move on."
  • Breaking: Renamed the client Keycloak role to participant, to avoid confusion with the unrelated Keycloak concept of a client (an OAuth2 application registration, e.g. the dlb-web-service client itself) — client was never meant to denote "a service acting as a client," only "a basic user who can execute dialogues," which participant states unambiguously. AuthenticationInfo.USER_ROLE_CLIENT is now USER_ROLE_PARTICIPANT (value "participant"), and every end-point role check, the dev-stack's dialoguebranch-realm.json, and the docs (authentication.adoc, dlb-web/index.adoc, both installation/exploring-the-API tutorials, README.md) were updated to match. This is a clean break, not backwards compatible: any already-provisioned Keycloak realm (local dev instances that already imported the old JSON, staging, production, or any third-party self-hosted deployment) has this role under its old name, client, and must be manually renamed in the Keycloak admin console — the dev-stack's --import-realm only seeds a fresh realm, so this JSON change does not retroactively fix already-provisioned Keycloak instances. Existing client-role users will get 401 INSUFFICIENT_PRIVILEGES from every endpoint until their role is renamed.

Fixed ​

  • Fixed the Web Client's Dialogue Browser not alphabetically ordering dialogues in Authoring Mode (#72). Both sort comparators (DialogueBrowser.vue's top-level tree and DialogueTreeNode.vue's recursive children) only ever partitioned folders before files — same-category entries always compared as 0 (equal), so any apparent alphabetical order was purely incidental, preserved from whatever order the underlying API happened to return. That's a coincidence Live Mode's /dialogue/list-dialogues response order usually matched, but Draft/Authoring Mode's /authoring/list-dialogues doesn't. Both comparators now break ties with localeCompare on the folder/file name, so ordering no longer depends on the API's response order at all.
  • Fixed the Web Client's IconButton.vue disabled prop only changing the button's cursor styling instead of actually disabling it — the native <button> element was never bound to :disabled, so a fast double-click could still fire @click on a "disabled-looking" button (e.g. mid-request, on the "Refresh current dialogue step"/"Refresh dialogue list" buttons, or the Dialogue Editor's "Cancel dialogue"/"Revert variables"/"Add Node" buttons).
  • Fixed the Web Client's Dialogue Browser collapsing every expanded folder on every refresh, even when the refreshed list is identical to what's already shown — the common case (#71). listDialogues() now snapshots the displayed entries (name, isPublished/isNew/isChanged/isDeleted, order-independent) and only resets openFolders when a refresh's result actually differs from that snapshot — triggered by manual refresh, publish, and creating/renaming/deleting a draft dialogue, not just the initial load. Since a no-op refresh no longer has any visible effect on its own, the "Refresh dialogue list" button now spins while the request is in flight (with the same minimum-visible-duration treatment as the "Refresh current dialogue step" button), so clicking it still gives feedback that it did something.
  • Fixed the Web Client's "restart dialogue" button (shown once a live/published dialogue test has finished) opening a new tab instead of restarting in the current one (#70). DialogueWorkspace.vue's loadDraftDialogue already accepted an existing tab to restart in place — used by restartActiveTab for draft tests — but loadDialogue (the live-dialogue path) had no equivalent parameter, so getOrCreateEmptyTab() always ran, and since the just-finished tab still has a dialogueName set, it never counted as "empty" and a new tab was created every time. loadDialogue now accepts the same { tab, language } option loadDraftDialogue does.
  • Fixed a project being parsed and loaded into memory twice on a fresh/empty database boot, e.g. Loading Dialogue Branch project 'default-test' into memory. / Successfully loaded... logged twice in a row before the seed-completion message. ProjectSeedService seeds a new project by publishing it via PublishService.publish(), which itself calls ProjectLoaderService.loadProject so a live publish takes effect immediately — but ProjectLoaderService.loadOnStartup() (@Order(1), runs right after seeding's @Order(0)) then unconditionally reloads every project in the database, redoing the exact same just-finished load. loadProject now skips reloading a project whose exact version is already in ApplicationManager, logging Project 'X' version N is already loaded — skipping. instead. Harmless before (the second load was a correct, idempotent replace) but wasteful and confusing in the logs; only visible on a first-ever/empty-database boot.
  • Fixed the API's Swagger UI showing "1" (the latest API protocol version) as the document version instead of the actual software version (e.g. 2.0.1). OpenApiSwaggerConfig now reads the version from DlbProperties.getVersion() instead of ServiceContext.getCurrentVersion().
  • Fixed inconsistent "DialogueBranch" branding (missing space) throughout the platform — Swagger UI strings, CLI prompts and error messages (ProjectTool/CommandLineRunner, POEditorTools), log messages, exception messages, source comments and license headers, web.xml, and documentation prose — now consistently read "Dialogue Branch". Class/package/method names (e.g. DialogueBranchParser, com.dialoguebranch) were intentionally left unchanged, as were a couple of literal "DialogueBranch" strings in examples/project-test translation fixtures that are test content tied to an example dialogue script, not branding.
  • Removed apps/api/src/main/webapp/WEB-INF/web.xml, a leftover Java EE deployment descriptor from before the service was rebuilt on Spring Boot's embedded server. apps/api/build.gradle doesn't apply the war plugin, so src/main/webapp was never packaged by the build or read at runtime; the root-path redirect to Swagger UI it once implied is already handled by SwaggerController and static/index.html.
  • Fixed DialogueExecutor resolving cross-dialogue reply links ([[link|OtherDialogue.Node]]) by searching all loaded projects for a matching dialogue name/language, instead of scoping the search to the originating dialogue's own project — even though ExternalNodePointer (the model class backing these links) only ever resolves relative paths bounded at the project root, so a cross-project reference was never a real possibility to begin with. If two projects happened to declare a same-named dialogue in the same language, a link could silently resolve into the wrong project's dialogue. DialogueExecutor now calls the project-scoped getDialogueDescriptionFromProject/getDialogueDefinitionForProject (passing the originating serverLoggedDialogue's project slug) instead of the unscoped, all-projects variants. This also let a whole cluster of now-unreachable code be deleted: UserService's getAvailableDialogues/ getDialogueDescriptionFromId/getDialogueDefinition(ResourcePointer), and ApplicationManager's getDialogueDescriptions/getAvailableDialogues()/ getAvailableDialogues(String)/getDialogueDefinition(ResourcePointer, TranslationContext).

[0.1.1] - 2026-07-16 ​

Added ​

  • Added an optional delegateUser parameter to the API's /draft/* end-points (start, progress, cancel, revert-variables), matching the existing /dialogue/* and /variables/* end-points. This lets an admin test-run a draft dialogue on behalf of another user instead of only their own account.
  • Added a global Live Mode / Authoring Mode toggle to the Web Client header (to the left of the Project menu), replacing the previous behavior where the Dialogue Browser always showed a merged list of published and draft dialogues and the node-graph editor could be opened from any tab regardless of context. In Live Mode (the default), the Dialogue Browser lists only published dialogues, testing always runs against the published /dialogue/* end-points, and the node editor is unavailable. In Authoring Mode, the Dialogue Browser lists only draft dialogues — with their New/Changed/Deleted status and rename/restore controls — testing runs against the ephemeral /draft/* end-points, and the node editor becomes available. "Publish Project" is now only actionable in Authoring Mode. Switching modes clears all open dialogue tabs.
  • Added small icons in front of the "Mode", "Project", and "User" labels in the Web Client header menus, for easier visual scanning.
  • Added a mouseover tooltip ("Refresh dialogue list") to the Dialogue Browser's refresh button.
  • Added a "Test dialogues in:" label before the language selection dropdown in the Dialogue Workspace toolbar, vertically centered alongside the other toolbar controls.
  • Added a fourth Translate mode to the Dialogue Workspace's mode switcher (alongside Speech Bubble Test, RPG Text Test, and Edit), available only in Authoring Mode. It lists every translatable term in the active tab's dialogue, grouped by speaker, with an editable cell per translation language and per-cell autosave (mirroring the Variable Browser's dirty-tracking pattern) — no external export/import round-trip needed to edit translations anymore. Since showing every project translation language as its own column stops scaling once there are more than a couple, the user instead picks which one or two languages to display via "Column 2" / "Column 3" dropdowns (Column 3 can be hidden to save space). Backed by two new read-only API end-points, /authoring/get-translation and /authoring/list-translatable-terms.

Fixed ​

  • Fixed two literal NUL bytes accidentally embedded in DraftDialogueService.java (in place of plain space characters used as a lookup-key delimiter during dialogue rename). They rendered as ordinary spaces in editors and didn't affect runtime behavior, but caused the file to be misdetected as binary by tools like grep, silently breaking plain-text search across it.
  • Fixed an issue in the Web Client where the delegate user selected via the admin's delegate-user picker was not sent to the API's /draft/* end-points, so testing a draft dialogue while impersonating another user silently ran against the admin's own account instead.
  • Fixed an issue in the Web Client, where testing dialogues would always use the /draft/* end-points, even when the project had no unpublished changes. Now, the Interaction Testers correctly use the default /dialogue/* end-points for testing dialogues in projects that are fully published. If any single change is made to any dialogue, the interaction testers switch to "draft test mode", to make sure those new changes are taken into account. This means that if a change is made in Dialogue A, and the user starts a test on Dialogue B, this test will still run using the /draft/* end-points, as this dialogue could refer to new contents in Dialogue A.
  • Fixed an issue in the API where /dialogue/continue (and /dialogue/progress) could incorrectly respond with "Dialogue not found" for a dialogue that does in fact exist. The lookup used to resolve an ongoing dialogue was reading from a per-user cache that is only built once when a user's session starts, so any dialogue published after that point was invisible to it. This lookup now resolves dialogues live, scoped to the ongoing dialogue's own project, matching how /dialogue/start already worked.
  • Fixed an issue in the API where /dialogue/get-ongoing and /dialogue/continue could offer to resume a dialogue whose project had since been republished with different content, even though the dialogue itself still existed under the same name. Each logged dialogue is now pinned to the published version of the project it was started against; if that version is no longer the project's current one, it's treated as stale and no longer offered as resumable.
  • Fixed an issue in the API where requesting a dialogue in a specific translation language (e.g. nl-NL) via /dialogue/start could silently return the source-language content instead, because the language-matching logic never correctly matched against a project's hyphenated language codes and fell through to an unrelated default. Language matching is now an exact match against a project's declared languages, falling back to the source language when there's no exact match — no more silent mismatched-language fallback.
  • Aligned /draft/* dialogue testing with the /dialogue/* behavior above: requesting a language a draft dialogue has no translation for now falls back to the source language the same way, instead of rejecting the request outright.
  • The Web Client will now always switch to the Ephemeral Draft Testing Mode after any dialogue in the project has been edited, even if the change only consisted of moving the position of any node. NOTE: This also means that any "move node" event is immediately saved to the server.
  • The "LoggedDialogueId" in the bottom of interaction testers is now correctly cleared when switching to "Ephemeral Test Mode".
  • Fixed a term-matching bug in the new Translate mode where a term whose source text spans multiple script lines (e.g. two paragraphs separated by a blank line) always appeared untranslated, even when a matching translation existed. Term keys were built with Translatable.toExportFriendlyString(), which only trims the ends of the text, while the actual translation-matching engine (Translator) normalizes all internal whitespace (including line breaks) to single spaces before comparing — so a multi-line term's key never matched its stored translation. Extracted that normalization into a new shared Translatable.toNormalizedString() (packages/core) and pointed both Translator and the term-listing end-point at it, so term keys are now generated identically everywhere.
  • Fixed the Debug Console showing double-encoded JSON fields (e.g. DBDraftTranslation.content, itself a whole JSON document stored as a string) as one long line packed with escaped quotes instead of being indented like the rest of the body. prettyBody() now recursively parses and re-indents any string value that looks like a JSON object or array.

Changed ​

  • /draft/* end-point session IDs (draftSessionId) are now generated the same way as loggedDialogueIds: a bare 32-character hex string (dashes stripped from the underlying UUID), instead of the standard hyphenated UUID form. Purely cosmetic — the ID is only ever used as an opaque token by both the API and the Web Client.
  • Logged dialogues are now stored in the database instead of as JSON files on disk, mirroring how Dialogue Branch Variables are already stored. This also makes looking up a user's most recent ongoing dialogue a single indexed database query instead of a scan of every session file.
  • When in the "Balloon Interaction Tester" mode, the avatar image is now carrying some tools when you are testing in "Ephemeral Draft Test" mode, just to make this more visually clear.
  • Both the "Balloon Style" and "Text Style" interaction testers show a message "Ephemeral Draft Test"
    • Session ID: X in the bottom of the panel to indicate that this mode is active.
  • Renamed the "Draft" badge shown in the Dialogue Browser for a dialogue with unpublished changes to "Changed" — now that Authoring Mode only ever lists draft dialogues, labeling one of them "Draft" no longer distinguished it from the rest of the list.
  • Renamed the Dialogue Workspace's "Balloon style" and "Text style" mode button tooltips to "Test in Speech Bubble Style" and "Test in RPG Text Style", respectively.
  • Squashed the API's ten Flyway migrations (V1__create_schema.sql through V10__add_draft_dialogue_previous_published_name.sql) into a single V1__create_schema.sql reflecting the current end-state schema. Since the API has not yet been deployed anywhere with data worth preserving, there was no reason to keep the intermediate steps (e.g. a table added and later dropped, a column renamed and then renamed again) around as migration history. Any existing local MariaDB volume must be reset (docker compose down -v) before starting the API against this new baseline, since Flyway will otherwise detect a checksum mismatch against the old migration chain.

Removed ​

  • Removed the unused JSON-file-based VariableStoreJSONStorageHandler and the VariableStoreStorageHandler interface it existed alongside. The interface had only ever had one real implementation (VariableStoreDatabaseStorageHandler, which the API wires up directly by concrete type), so the JSON handler was never instantiated or reachable at runtime; consumers now depend on VariableStoreDatabaseStorageHandler directly instead of the interface.
  • Removed the unused DBDraftDialogue.renamedFrom field (and its renamed_from column, dropped via V9__drop_draft_dialogue_renamed_from.sql). It was meant to let the next publish know which published entry to drop after a rename, but that was never implemented — each publish already writes a full, independent snapshot of the live drafts under their current names, so there was never anything to look it up for. It was faithfully set on rename and cleared on publish, but never read in between.

Security ​

  • Fixed a missing authorization check in the API where any authenticated editor or admin could progress, cancel, or revert another user's /draft/* test session simply by obtaining its draftSessionId, since the session lookup never verified it belonged to the requesting user. Draft test sessions read and write the tester's real Dialogue Branch variables, so this could let one user manipulate another user's variable state without their knowledge. /draft/progress, /draft/cancel, and /draft/revert-variables now verify the session belongs to the requesting (or, for admins, delegated) user before acting on it.