Changelog
What changed in each version: what was added, changed and fixed, and what to check before you upgrade. Before an upgrade, read the "Upgrade notes" and "Breaking" entries of the version, then make a backup as Data, backup and upgrade describes.
Entries for the next release are kept in docs/changelog.d/ of the repository until it is published.
Unreleased
Entries for the next release are in changelog.d/, one file per change; do not add lines here. pnpm changelog:preview prints them as this section.
[0.0.53] - 2026-10-28
This release runs the desktop backend on the app's own runtime, opens a public web trial and refreshes the website. The desktop backend now runs on the packaged Electron 45 executable with RunAsNode instead of a separate Node download; every start, restart and recovery verifies the runtime, the main bundle and the preload bundle, and Windows gains an extract-once ZIP download. macOS builds require macOS 13.5 or later. Source and Docker deployments move to Node 26.12.1 and patched HTTP clients for the October 2026 security fixes, and the Docker image starts one Node process. The Render blueprint now deploys a playable trial in the browser-private form, the official one at demo.covel.cc: saves stay in each player's browser, API keys travel only with each request and are never written to disk, a database or logs, and that form now leaves open only what visitors do with their own keys and saves. Players there see a notice that links to Play on a public trial and to the exact source commit, and can play with a model on their own computer or local network, which their browser relays to the server. A new browser save keeps the default plugins and the world's starting quests and items from its first sync, and forms and plugin panels shown during a turn unlock when the turn ends. The website gains live plugin examples, a download catalog for every platform, a language menu and a link to the trial. The getting started guide now opens with a language choice, and Dice Check no longer rolls against health or trust in a world without abilities. Plugin panels name turns by number, format dates and show character types, item tags and time rules in the player's language; agents that read World IR keep the links between characters with UUID IDs and the relations that name them, and world translation no longer depends on a model copying long IDs. Read the breaking changes and the upgrade notes before installing. macOS Apple Silicon and Windows x64 binaries are unsigned; the macOS build is not notarized.
Verified while preparing this release, on Linux x64: pnpm check, every Vitest and Node test suite (turbo test --continue: 43 of 44 tasks passed; the desktop suite passed 65 of its 66 tests, and the remaining one could not launch Electron because the machine lacks the system library libnspr4.so), pnpm release:preflight, pnpm test:docs:packages, and the documentation and website builds. The browser-private trial ran on Render's free plan from the merged blueprint, and its /api/health reported the browser-private form and the deployed commit. The local model relay had an adversarial security review before release; its findings (server-owned keys reaching a browser, uncapped relay streams, encoded path separators, plugin wires holding the relay, head-handling races and caches shared across players) were fixed with regression tests. Real play through the relay then found a truncated page, gameplay model calls that dropped the relay, a first browser sync that discarded the default plugins and starting content, and in-turn forms and plugin panels that stayed locked; all four were fixed with regression tests before the tag. After those fixes, with a real model through a local Magpie Responses endpoint: a production Docker image with the Render blueprint's settings and a 512 MiB limit was played in a browser through the relay (a new Emberback save with its default plugins, two turns, a container restart that restored the save, and a third turn, while the container itself could not reach the player's loopback port), the packaged macOS arm64 app was played, quit, relaunched and continued, and both completed Harukaze Academy's character creation, opening and a turn. On the release candidate, CI passed the static checks, every unit suite, pnpm test:pg against PostgreSQL, pnpm e2e:smoke with the community plugin fixtures, the website build and CodeQL. The first Build Desktop trial runs found Windows-only failures in desktop test fixtures and in the Windows shipped-artifact check, all fixed before the tag; the trial run of those fixes built the macOS arm64 and Windows x64 packages, passed the Windows desktop regressions, and booted the shipped Windows ZIP and portable EXE to session preparation. The same Windows steps also passed on a local Windows 11 VM. On native macOS arm64 at e7217a3c7, pnpm --filter @covel/desktop smoke:restart passed with the real preload, IPC and origin guard, including navigation to a new backend port and version checks before and after restart; it uses two isolated HTTP fixtures, not a real model or a packaged backend restart. The macOS arm64 trial package from that commit's Build Desktop run was downloaded, its artifact digest checked, and its actual ZIP application verified locally: bundled runtime and resources, SQLite and vector support, builtin plugin loading with and without model configuration, world selection and session preparation, page reload, preload version 0.0.53, and clean process-group exit. On Linux x64 with the mise toolchain, pnpm --filter @covel/desktop build passed its staging boot check, pnpm test:pg passed against PostgreSQL 18 with pgvector, and the full pnpm e2e passed 140 tests; one AI world-generation test that relies on a one-second model timeout fails intermittently under load, as it does on v0.0.52. A last round of fixes from a local audit (plugin panels, the model-facing projection of linked IDs, translation IDs, the API key file text and the release workflow's ref check) was then verified on its final code commit 8a32ac128: pnpm check, all 44 Vitest and Node test tasks including the desktop suite, pnpm test:pg against PostgreSQL 18 with pgvector, the full pnpm e2e (142 passed, 6 skipped), the documentation package tests, the pull request's CI, a Build Desktop trial on macOS arm64 and Windows x64, the Windows release steps with the shipped ZIP and portable EXE on the local Windows 11 VM, and the changed panels in a browser with the bundled worlds in Chinese and English. Follow-up fixes for multilingual time rules in worlds without a package directory, records that repeat their own UUID, and the workflow's guidance for SHA inputs were verified on 8226fd867. On macOS arm64: pnpm check, pnpm check:push, the changed packages and their dependents, the documentation build, new workflow tests, a new production-browser E2E of the English and Chinese time panels, pnpm e2e:smoke, the desktop tests and smoke:restart, and a locally packaged unpacked Covel.app (its own RunAsNode backend, durable SQLite with sqlite-vec, plugin loading, the time panels, page reload, a full restart with persistence and a clean exit). On Linux x64: pnpm check, all 44 test tasks, pnpm test:pg, the full pnpm e2e (146 passed, 6 skipped) and the documentation package tests, plus the pull request's CI, a Build Desktop trial on macOS arm64 and Windows x64, and the Windows release steps with the shipped ZIP and portable EXE on the local Windows 11 VM. The live HTTPS trial at demo.covel.cc was updated to f5baa9b42; in a real Mac browser it completed relay pings over HTTPS and SSE, page navigation, two player actions and transcript recovery after a full reload, with a keyless synthetic model on localhost. Not verified yet: the browser's first permission prompt and the host's cold start on the live trial; any play on Windows; Gatekeeper or DMG installation; and the published installers with their checksums and provenance.
Breaking changes in v0.0.53
- macOS desktop builds now require macOS 13.5 or later. This declared minimum remains in place when the backend uses Electron RunAsNode.
- Desktop staging checks no longer accept
--electron-nodeorCOVEL_SMOKE_HOST_NODE. They always verify and run bundled Electron with RunAsNode and SQLite; missing or mismatched runtime resources fail the build.
Added
- Windows has an extract-once ZIP download. Extract the folder once and run
Covel.exefrom it; the installer remains the recommended download. The single-file self-extracting EXE remains available with a native preparation notice during extraction. See installation. - Players on a public trial see where their saves and keys go. When
/api/healthreports a shared server whose saves stay in the browser, the web app shows a dismissible notice: saves and API keys are kept in the browser, the server never writes keys to disk, a database or its logs, and the server may restart at any time. It links to the new "Play on a public trial" page and, when configured, to the exact source commit the instance runs. The notice, the guide's model step and each provider's key form in Settings → LLM → Providers & Models also ask players to create a temporary key for the trial and to delete or rotate it at their provider afterwards; the key form keeps the docs link after the notice is dismissed. /api/healthreportsdeployment.deployment.formispersonal,browser-privateorhosted;deployment.sourcecarriesCOVEL_SOURCE_REPOSITORYandCOVEL_SOURCE_COMMIT(falling back to Render'sRENDER_GIT_COMMIT) when they are valid. Fields:docs/reference/api.md.- The website and the READMEs point to the official web trial. The homepage hero has a “Play in your browser” button and the downloads section links demo.covel.cc; both READMEs describe the trial, what stays in the browser and its free-plan limits.
- The getting started guide opens with a language choice. Its first step lists every interface language in its own script and switches the interface as soon as one is chosen; the language drop-down above each step is gone. The guide is new, so players who finished an earlier one see it once more; their model settings are kept.
- A public trial can use a model on the player's own computer or local network. On a shared server, a provider endpoint such as
http://127.0.0.1:3425/v1,http://localhost:11434/v1orhttps://192.168.1.20/v1is reached through the player's browser (the local model relay): the server hands the request to the browser overGET /api/model-relay/stream, and the browser calls the local model and posts the answer back. Public endpoints are still called by the server. The model server must allow the trial's origin through CORS, and the page must stay open; plain-http LAN addresses are not supported because browsers block them from an HTTPS page. Settings explains each case under the endpoint field. Steps:docs/start/hosted-trial.md; endpoints:docs/reference/api.md. - Download any supported platform from the website. macOS and Windows builds are listed together with individual download and copy-link controls, regardless of the visitor's operating system. Windows reserves installer, ZIP and portable formats, recommends the installer, and marks unreleased files without inventing download links.
- Explore plugins directly on the homepage. Roll dice, advance a quest, inspect relationships and search the codex in localized, theme-aware browser examples.
- The homepage foregrounds plugins, world packs and creation. In-app world generation and workspace Agent Skills have distinct introductions, with small links to the growing plugin/world directories. Downloads are consolidated in one section, reached by sticky quick navigation.
- The plugin examples now include an interactive relationship graph. Character profiles and connections are separate from player affinity; optional image generation and speech synthesis extensions are introduced alongside the examples.
- Authoring skills have a copyable installation command. The homepage retains direct SKILL.md links, and download formats use a single compact platform catalog.
- Homepage scenes follow each world’s actual plugin selection. Three selectable moments show replies, checks, quests, world panels and their sample outcomes, with synchronized plugin highlighting and links to the world configuration.
Changed
- Desktop backend verification reads large runtime files asynchronously before starting the server. Every start, restart, and recovery still checks the pinned runtime and main bundle afresh, while file reads yield to the desktop event loop. Failed checks still stop startup and request a complete reinstall.
- Each desktop staging smoke checks the Electron fuse once before probing its versions. Resource validation and the version probe share one preflight; later launches, restarts and recoveries still perform fresh checks.
- Desktop backends use the packaged Electron 45.0.0 executable with RunAsNode. The backend remains a separate child process with private lifecycle IPC. Standalone Node downloads, the extra executable and license, and runtime-selection configuration are removed. Source and Docker deployments keep Node 26.12.1; macOS still requires 13.5 or later.
- Windows release validation exercises Electron 45 directly. Desktop regressions run on Windows, and the shipped ZIP backend is checked after extraction into a path with spaces and Chinese characters. ZIP/portable UI checks cover reload and record artifact sizes. Backend smoke checks bound HTTP requests, wait for child termination before cleanup, and retain ZIP backend logs on failure.
- Desktop packages carry fewer redundant files. Bundled main-process dependencies are no longer copied again, and server staging removes the duplicate pnpm tree plus reviewed development-only files after compilation. TypeScript community plugins, native database support, authored content, locales and license notices are retained. This reduces payload size and file count; it does not establish a Windows startup-time improvement.
- The Render blueprint deploys a playable public trial on the free plan.
render.yamlnow runs the browser-private form (DEPLOYMENT_TIER=self,STORE_BACKEND=memory) instead of an operator-onlydemohost, listens on Render's port 10000, caps the V8 heap for 512 MB instances and names its source repository. Steps and limits:docs/start/deploy-render.md. - The Docker image starts one Node process. The container runs
node --import tsx src/index.tsinstead ofpnpm exec tsx, which saves about 100 MB at idle and delivers stop signals to the server directly. operatorTokenRequiredistrueon every shared form. A browser-private server now reports it, so the web settings show "Operator Access" and hide the media library there, as on hosted tiers.- The website now uses editable, localized HTML previews. Gameplay and world-creation examples follow the page theme and language, with reusable world art, consistent icons, polished Chinese and English storytelling copy, and a pause control. Scenes crossfade on a twelve-second cycle with restrained entrance motion and stable layout.
- Website languages are selected from a menu. The theme-aware custom picker supports keyboard selection. The language registry supports additional editions, and selections persist in the URL and local preference.
- The website opens in dark mode by default. Manually selected themes and languages are remembered across refreshes and return visits; the saved theme is applied before the first paint.
- The homepage separates play experiences from shared features. Its fourth scene now presents Emberback Relay’s survival sandbox; multilingual support has its own feature panel.
- Stable dependencies and development tools are refreshed. Updates follow the repository's release waiting period and include pnpm, Vitest, Playwright, and the tools scaffold. See the dependency policy in Package boundaries.
- The author SDK requires Node 26.12.1, like the host.
@covel/plugin-handlers-utilsnow declaresengines.node>=26.12.1 <27, the same minimum as the repository, CI and the Docker image, which includes the October 2026 security fixes. Plugin projects on an earlier Node 26 release should move to 26.12.1. - Build Desktop refuses a ref whose release or CI workflow differs from the one running. Building an old tag from
mainused to fail late withMODULE_NOT_FOUND, because the newer steps call scripts the old source does not have. The run now stops while resolving the source and says to run the workflow from that ref (gh workflow run release.yml --ref <ref>). Seedocs/internals/desktop-packaging.md.
Fixed
- SQLite vector search stays available in the Alpine Docker image. The image preloads its installed glibc compatibility library so
sqlite-veccan resolve its required symbols under musl. An offline build check loads the extension as the runtime user and fails the build if it cannot load. - Windows desktop backend launches hide the extra Node console window. Starting or restarting the managed backend keeps its console hidden.
- Desktop health checks release unused response bodies. Repeated readiness and heartbeat checks no longer retain connections for bodies they never read.
- Desktop package management explains failed restart checks and offers a retry. Restart stays unavailable while desktop information is loading or failed, and becomes available after a successful check in a packaged app.
- Desktop resource validation also checks the preload bundle. Builds, packaged ASAR checks, and managed backend launches reject missing or mismatched preload bytes alongside the main bundle. Rebuild the complete desktop package; existing saves do not need recreation. These digests detect mixed or damaged builds and do not provide publisher signing or trusted-root tamper protection.
- Dice checks without a usable ability no longer show an attribute label in the instruction language. When no ranged ability is available, the check rules use modifier 0 and omit the optional
attributefield from the receipt.COVEL_INSTRUCTION_LOCALEtherefore cannot insert an English "None" into a Chinese session or a Chinese label into another language. See RPG mechanics and plugin language rules. - Dice Check no longer rolls against health or trust in a world without abilities. When the player character has no ranged number in the
abilitiescategory, the check rules use modifier 0 and report no attribute, instead of asking the narrative to invent a modifier from health or trust. Missing character data uses the same rule. - Development desktop shells no longer offer to restart an externally managed backend. The Desktop and Install & manage panes explain that the developer must restart
pnpm dev; direct restart requests are rejected without changing the server or port. - Quick desktop restarts cancel an in-flight update check. A stalled GitHub response no longer consumes the backend's shutdown budget and prevents its storage-close phase.
- Recursive JSON schemas type-check with TypeScript 7.1. Update Zod to 4.6.7 across the host, author SDK peer dependency, and tools scaffold, and give the tool-invocation factory an explicit portable return type. Runtime validation and plugin contracts stay unchanged; stable TypeScript 7.0.2 remains the project compiler.
- Browser-private saves retain default dependencies and world starting content. The first sync downloads the server's complete initialized state instead of overwriting it with an empty local seed. Quests, inventory, affinity, codex, and relationship features keep their required providers and starting data. Established saves keep their own selection and progress when the server restarts.
- Character forms and plugin panels shown during a turn become usable when the turn finishes. Their actions now run the current handlers instead of the ones captured when they appeared, so a stage panel's message button no longer stays inert after the turn; entered form values remain intact.
- Dice Check's check log names the turn by its number and the attribute by the world's name. Each entry showed the turn's internal ID, and the attribute exactly as the narrative wrote it (an attribute id in one entry, the attribute's Chinese name in the next). Entries now read "Turn 7" in the UI language, and an attribute that matches one of the world's character attributes, by id or by name, shows that attribute's name; other text is shown as written. The in-message check block shows the same attribute name. Checks recorded before this change show neither the turn nor the attribute; new checks in the same session show both. See
docs/builtin/rpg.md. - Settings names the right file for API keys. The desktop app says keys are saved in
keys.envin the Covel config folder; the web app says keys entered there stay in the browser and that the server's own keys come from its environment, such as.env.llm. The Chinese text namedkeys.envand the English and Russian texts named.env.llmon both. - Chinese sessions no longer show English character types and item tags. The character panel's type badge shows "Player", "NPC" and "Companion" in the UI language; a type the world declares itself is shown as the world writes it. The Chinese editions of Lantern Barrow and Emberback now write their item tags in Chinese, and their English editions keep English tags. Inventory's authoring hint now says that players see the tags, so a world writes them, money's currency tag included, in its own language. Sessions created before this change keep the tags they imported. See
docs/recipes/starting-content.md. - The Core Memory panel shows when a block changed as a short local date and time. It showed the raw ISO timestamp, such as
2026-10-28T09:15:00.000Z; the stored value is unchanged. - Memory extraction no longer sends the model a turn ID inside a form ID. The prompt section for the player's latest form now holds only the values the player entered, still marked as data that may belong to an earlier turn. It used to include the whole submission, so a form ID such as
tabletop-rules-check-<turn ID>reached the model. - Agents that read World IR can tell who a relation or event names when characters have UUID IDs. The model-facing projection used to drop each entity's UUID
idbut keep the relation endpoints, item holders and mixed participant lists that point to it, so downstream agents such as NPC Graph saw IDs they could not match to a name.modelFacingJsonnow keeps a UUID that links one part of a value to another, wherever it appears; UUIDs that nothing in the value names (rows, turns, results) are still left out. Rule:docs/reference/plugins.md. - World and plugin translation no longer fails when a model miscopies a text's ID. Each request now shows the model short numbers instead of IDs such as
characters/characters.json#[id=<UUID>].name, and maps the answers back, so a character with a UUID ID translates like any other. - World Time's "Time rules" panel summarizes the rules instead of listing raw fields. It showed every field of the world's time definition (
kind,initial,defaultStep, …) and repeated the timing prompt. It now lists the phases and cycle, or the era, months, hours and weekdays of a calendar; where time starts; which way it moves; and how far it moves in a turn by default and at most, with labels in the UI language. Seedocs/builtin/world.md. - Time rules show multilingual names from portable world data. Cycle and era names, phases and weekdays are resolved before display, including worlds without a package directory.
- A record repeating its own UUID through arrays or nested fields stays bookkeeping. Independently identified child records still preserve references to their parent; model-facing projection and hidden-field diagnostics share the rule.
- Desktop build failures give usable workflow-ref guidance for SHA inputs. Select a branch or tag pointing to the required commit; workflow dispatch does not accept a commit SHA as its ref.
Security
- Desktop runtime verification fails closed across builds, packaging, startup, and restart. Verify actual embedded versions and RunAsNode capability, bind target resources to the main bundle, check packaged ASAR bytes before release smoke, and strip inherited runtime-control environment variables without removing legitimate CA, proxy, or provider settings.
- Release verification uses a patched ASAR reader and bounded brace expansion. Pin the build-only archive reader to
@electron/asar4.3.1 and restrict thebrace-expansion5.0.12 override tominimatch10, leaving older dependency lines unchanged. - Electron 45 includes the eMudhra certificate blocklist and the verified Chromium Brotli decoder fix. Its embedded Node 24.22.0 is not equivalent to the full Node 24.22.1 security release. Core npm Undici 8.12.2, URL normalization, and
dns.lookupreduce exposure, while native plugin networking and specialized direct PostgreSQL configurations retain residual risk. See the runtime boundaries indocs/internals/security.md. - Desktop packaging disables Node CLI debugging arguments and restricts the app entry point to ASAR. RunAsNode and custom-CA environment support remain enabled; managed child startup strips
NODE_OPTIONS. These fuse settings do not provide signing or cryptographic ASAR integrity verification. Loopback sidecar HTTP requests also reject redirects. - Source and Docker deployments use Node 26.12.1. The minimum supported Node 26 version, mise, CI and Docker pins now include the October 2026 security fixes, including the native Brotli decoder fix. The Docker image remains pinned by digest.
- Malformed compressed HTTP responses no longer terminate Covel through the affected Undici client. Provider traffic and the desktop shell's sidecar client use npm Undici 8.12.2. Transitive Undici 6.x and 7.x copies receive their corresponding security releases without changing major versions. Exact reviewed security versions bypass the usual seven-day dependency delay.
- The browser-private form keeps open only what visitors do with their own keys and saves. Approving or running community plugin server code, diagnostics (including
GET /api/provider-keys), model database refresh, host configuration and media cleanup now need the operator token, and server settings cannot be written. Creating sessions, generating worlds and listing or testing models stay open. Model endpoints may no longer resolve to loopback or private addresses there, and the Host check is off because session owner tokens are the boundary. Rules:docs/internals/security.md#host-profile. - The local model relay only carries what the player configured. A request reaches the browser that named its random channel in
X-Model-Relay, never another visitor's, and only when its endpoint came from the player's own settings with no server-owned key or header attached: the server's own model addresses and keys are never shown to a browser. The browser makes a request only when the URL is under one of the player's own provider endpoints on their computer or local network, with GET or POST, without cookies, without encoded path separators and without following redirects; anything else is refused without a request. Plugin wires never receive the relay. A server in a shared form addshttp://127.0.0.1:*andhttp://localhost:*to the page'sconnect-srcwhen it serves the page; the built page, personal servers and the desktop app keep the policy unchanged. Opening a channel is rate-limited like the event stream, a process holds at most 512 channels, and a reconnect closes the older stream. Rules:docs/internals/security.md#local-model-relay. - Malformed narrator-only comments no longer cause quadratic parsing time. Repeated marker mentions in world lore are scanned without repeated suffix backtracking, preserving existing spoiler-hiding behavior.
- Fallow is updated to 3.34.0. The development analyzer fixes out-of-root config reads and path probes when analyzing untrusted repository content.
Documentation
- Deployment guidance now distinguishes personal instances from shared ones. The API reference clarifies the desktop-token and browser-private exceptions to personal-instance authentication.
- New pages explain the public trial. "Play on a public trial" (
docs/start/hosted-trial.md) tells players what stays in the browser, what lives only in server memory, how their API key is used and who could obtain it. "Deploy a trial on Render" (docs/start/deploy-render.md) gives the operator's steps, free-plan limits and what not to configure.docs/internals/storage.mdlists the browser-private data that never reaches a checkpoint, anddocs/internals/security.mddocuments the in-memory credential handoff to background jobs. - Release acceptance covers real play through the local model relay, and the README stills show that play.
docs/internals/e2e-testing.mdgives a repeatable procedure for the packaged desktop app and a production Docker deployment: default plugins, two turns, a restart and a third turn, failure recovery, and records without personal paths or credentials. Both READMEs keep the demo GIF and replace their older stills with current gameplay screenshots taken that way, each in its edition's language: the English images come from English saves, since a session's content language is fixed when it is created. The guide also covers framing and screenshot clipping for later captures. - Magpie setup explains the extra requirements for public trials. The player guides now cover allowed web origins, real gateway keys, the key section hidden when sharing is off, browser local-network permission, and 403/401 failures, and distinguish the player's loopback address from Render and the desktop app's direct connection.
Upgrade notes for v0.0.53
- Rebuild the complete desktop application to receive the runtime change. Target and actual Electron/Node versions, RunAsNode capability, main-bundle/ASAR binding, and child environment cleanup remain verified. Players do not need a separate Node installation, and existing save data does not need recreation.
- Update the runtime as well as the source or dependencies. Source deployments need Node 26.12.1 or a newer 26.x security release; rebuild and restart Docker deployments. Desktop users need a rebuilt application to receive its Node, Electron, and HTTP-client updates; they do not need to install Node separately. No save-data recreation is required.
- A browser-private server refuses to start with
COVEL_ALLOWED_HOSTS. The Host check no longer runs on a production server withSTORE_BACKEND=memory, so the setting has no effect; remove it. - Create a new browser test save to verify starting content. Existing development saves are not reseeded automatically; their progress remains authoritative. A missing dependency can still be enabled from its in-game notice.
[0.0.52] - 2026-10-16
This release closes the findings of a whole-codebase audit and makes plugin manifests shorter. Bundled plugins get no interface a community plugin lacks, an agent guard gets the same context as a function handler, and a self-hosted instance answers only the host names it is told about. Manifests drop the fields that had one allowed value or only repeated a default (type, optional, the scheduled trigger, agent.llm, schedule.completion, …), a world keeps each character's roleplay notes in its character record, and a plugin names only a model role, never its own model or endpoint. Function runtimes use the model role they declare, AI world generation uses the player's story model, and a generated world no longer copies an NPC's identity into the player. The store now records the order in which writes were saved and drops an unused event column (data schema 6), so an SQLite or PostgreSQL database created by an earlier version or an earlier v0.0.52 development build is refused at startup and must be recreated; browser-private saves need nothing. Dead and duplicated code was removed across the packages. A failed setup or turn shows one recovery notice that names the provider and the other model uses routed to it, starting without a model key asks before the game begins, and a built-in remote provider without a key fails at once instead of sending the request. Magpie is a built-in local provider. Emberback's Chinese edition and the English editions of the three Chinese worlds were translated again for meaning, with plainer names; the worlds' rules state the modifier the dice actually use, and Dice Check offers only ability attributes for a check. Read the breaking changes and the upgrade notes before installing. macOS Apple Silicon and Windows x64 binaries are unsigned; the macOS build is not notarized.
Verified for this release, on the release candidate: pnpm check, pnpm test, pnpm test:pg, the full pnpm e2e suite (one stage-mode spec timed out under full-suite load and passed three of three runs alone), pnpm release:preflight, pnpm --filter @covel/desktop build with its staging boot check, pnpm --filter @covel/desktop smoke:restart on macOS, and a Build Desktop trial run that built the macOS and Windows packages. In a production web build on a fresh SQLite database, the Chinese edition of Emberback was played in a browser with a real model (codex/gpt-6-luna through a local Magpie relay): the missing-key prompt before the first start, adding Magpie in Settings, character creation with a rejected empty name, six turns with dice checks on ability attributes only, a page reload and a server restart that restored the game, and the narrow layout. That play found the Knowledge Codex dropping every entry after the UI-parts change, fixed before the tag. Earlier in the cycle pnpm e2e:verify with a real model passed on Mistport, Haruka Academy and Emberback, and Mistport, Haruka Academy and two worlds generated in the app were played in a browser. Not verified: the published installers were not started on Windows, no session was played in the desktop app, and no request went to a first-party OpenAI, Anthropic or Google endpoint.
Breaking changes in v0.0.52
- A background runtime that lists the
suspendtool no longer loads. A runtime withschedule.background: trueorschedule.detached: truethat listssuspendinagent.tools.builtinis rejected at load, because no player waits on background work. Removesuspendfrom such a runtime, or run it in the foreground. Rule:docs/reference/tools.md(suspend). - Agent completion and suspension follow the declared tools. A
tool-useruntime withafterToolsmust successfully call one of those tools; reads alone no longer satisfy completion, including after resume. WithoutafterTools, successful business tools still count. Completing-tool runtimes no longer receive automaticruntime-doneauthorization. Plugins that returned sentinel-shaped data from their own tools must instead declare and callsuspendorruntime-doneas appropriate. No stored-data migration is needed. MediaStore.resolveUrl()andctx.media.resolveUrl()have been removed. Plugin authors should usectx.media.get(ref)to read bytes or pass aMediaRefto the UI for display. Browser clients that need a fetchable URL useGET /api/sessions/:id/media-token. Custom media stores and test doubles should remove the method; existing media data needs no changes. See Media store.- The JSON file settings backend now requires an explicit IPC transport. Integrations must call
createJsonFileBackend({ ipc })or choose another settings backend; the REST transport options and server-managed secret markers have been removed. Desktop settings continue to use IPC, and browser settings use localStorage. Seedocs/reference/settings-store.md. - World packages no longer declare
schemaVersion. Remove it fromworld.yaml,data/world.data.yaml, andworld.data.override.yaml; the current format has no version selector. Loading ignores unknown top-level fields, andpnpm validate:worldwarns about them, so existing packages still load. Source records, plugin contracts, and stored-data versions keep their own version fields. Existing worlds and sessions do not need to be recreated. - Static tool policy accepts only
builtinandlocalsources. Host integrations must remove the unusedthird-partysource and its wildcard rule from their configuration and use the registry’s actual source. Registered community tools arelocal; community server-code and runtime authorization remain separate and unchanged. No stored session data needs rebuilding. - SQLite media paths are now relative to the media directory. Development databases containing absolute
media_assets.pathvalues must be recreated. There is no migration or compatibility read. Back up the old database and media before recreating data. - Remote theme resources no longer load directly. Reimport background images to embed them, or use local resources; remote fonts must be replaced. Theme imports and appearance edits download HTTPS raster images with CORS support, up to 5 MiB each. Allowed Markdown images use the same download path, so an image from a host that does not allow cross-origin reads (CORS) no longer shows, even after the player allows it. Existing settings backups do not download remote resources automatically. See
docs/reference/theme-packages.md. - A suspension also records what its saved writes would change.
pendingContinuation.takenUnder.writesholds, for every plugin data key, state field, character, character schema and lorebook entry the buffered proposals write, a digest of its content when the runtime suspended. The field is required: development suspensions and browser checkpoints that hold a suspension from an earlier build must be recreated. - Session event producers must capture their incarnation. Server EventBus instances log and drop untagged events, and subscription envelopes and oversized transport references carry
sessionIncarnation. Upgrade and restart all server processes together. Existing session metadata remains valid; no database recreation is needed. - Bundled plugins no longer get a store community plugins lack. A bundled function handler or agent guard used to receive a "trusted"
ctx.storethat read the world record and characters and wrote plugin data and characters directly. Every handler and guard now gets the same read-onlyctx.store. Read characters, the character schema and the world record throughctx.world, write plugin data throughctx.pluginData, and return other writes as proposals (withPendingProposals). Rules:docs/reference/plugins.md. - An agent guard gets the same context as a function handler. A community guard used to get only a read-only store: no
pluginData, logger, gateway, utils or media, andrecursiveCallwas refused. Every guard now gets the handler's context, includingctx.inputs,ctx.executionandctx.recursiveCall; a community plugin'sutilsstill reaches only the addresses itspermissions.httpdeclares. When the guard returns or times out,ctx.signalis aborted and these capabilities stop working, as they do for a handler. - Only the current development formats are accepted (F-073). Regenerate traces whose payload was wrapped in
data; recreate sessions/checkpoints containing duplicate open NPC relation versions or wrappeddice-checkrolls/message values. Recreate wrapped{ rule, ... }world-rule seed records as flat rules and reimport/recreate their affected session data. Recreate portrait records that rely on arbitrary session-prefixed character IDs; exact IDs and the currentnpc-<blueprintId>mapping remain valid. Normal current-format sessions need no rebuild. - Re-enter nested
settings.jsonentries.ui.localethrough the language selector (the stored key isentries["ui.locale"]). Rename barekeys.envprovider names to*_API_KEY, or re-enter those keys in Settings. Unrecognized lines are preserved, warned about, and never used as credentials. Provider-ID dictionaries in APIs remain supported. - Reimport custom themes after replacing
--surface-app,--surface-panel, and--surface-panel-strongwith--surface-page,--surface-rail, and--surface-elevated. The oldcovel:schemelocalStorage entry is ignored; currentui.schemesettings remain valid. All event transport publishers must be upgraded/restarted together becausestreamis required. POST /api/ai/generate-worldtakesbrief.packinstead ofbrief.experienceMode. The value is the ID of a pack of the catalogue; an unknown ID returns400. A brief withoutpacknames no pack: the world gets nopluginPolicy.preset(it used to gettraditional-story). Documented indocs/reference/api.md.- A kernel input takes only
from.io.inputs.<name>withfrom: { kernel: turn-digest@1 }rejectsscope,select,required,acceptsandrecordAs; they never did anything. Remove them from your RUNTIME.md. Seedocs/reference/plugins.md. - The game applies the full UI spec rules. A panel with a relative
repeat.statePathor an$item/$bindItemthat starts with/is no longer shown;pnpm validate:pluginalready rejected both. The host APIloadPluginUi()andLoadedRuntime.uiSpecsare gone; useloadPluginUiSpecs(). - The compiled runtime manifest no longer carries package fields.
RuntimeManifestdropsdisplayName,entry,extensions,tags,events,ui,i18n,commandsandworldProjections; the loader never filled them on a runtime. Read them from the package declaration (PLUGIN.md,ParsedPluginMd.manifest).userSettingsanddataSchemasstay, copied from the package. In/api/plugins, a runtime summary has notagsand a runtime contract has noui;/api/plugin-flowssteps have nouiSlots. Packagetagsare unchanged. - The in-code inject kind
runtimeis gone.input.injectno longer accepts{ kind: "runtime", from, field, as }, which noPLUGIN.mdcould produce. Code that builds manifests forexecuteTurnreads an upstream result with aninputsbinding (inputs: { name: { from: { runtime } } }).@covel/contextno longer exports the synchronousbuildInjectBlocks. - One JSON Schema for the compiled manifest.
runtime-manifest.input.schema.jsonis renamedruntime-manifest.compiled.schema.jsonand titled as the internal compiled form;runtime-manifest.authoring.schema.json,runtimeManifestAuthoringSchemaand theauthoringTriggerConfigSchemaalias are removed. Authors keep usingruntime-manifest.schema.json. store.getSession()returns a read-only session view, not the session record. Function handlers, guards, tools and plugin actions now getPluginSessionView:phase,completedPlayerTurns,localeandsetupRuntimes(each entrystate, plusresolution,lastError,reasonorwarningwhere they apply). Code that read another field of the record (id,activePlugins,status, …) getsundefined; take the session ID and language fromctx.sessionIdandctx.locale. If the session no longer exists,getSession()throws instead of returningnull. Fields:docs/reference/plugins.md.ctx.executionholds onlyoriginandsourceTurnId. The kernel'sexecutionId,logicalTurnIdandcountPolicyare no longer handed to plugin code.shortIdandshortIdBatchrequire the context'srandom, andctx.randomis never missing. The SDK no longer falls back tocrypto.randomUUIDorMath.randomwhen no random source is passed. Passctx.random(orcontext.randomin a tool) as the fourth argument; in tests,makeRandom()from@covel/plugin-test-utils.- A plugin can no longer name a kernel or world-package path as its data file.
contributes.data.<ns>.authoring.source.pathrejectsworld.yaml,WORLD.md,data/world.data.yaml,data/dimensions.yaml,data/lorebook.yaml,characters/characters.json,media/gallery.json,media/portraits.json,media/scenes.json,media/gallery/and their language versions, in any case. A manifest that names one fails to load; pick another path. Rules:docs/reference/world-data.md. ctx.recursiveCall()reads onlyplayerMessageandmanualTrigger: { runtimeId, payload }. Every other field of the delta is ignored: the nested run keeps the parent's session, language, player settings and model overrides, and a plugin can no longer pass retry seeds,sourceTurnId,runtimeIds,triggerEventordetachedStage. A wrongly typedplayerMessageormanualTriggeris rejected. Handler context:docs/plugins/local-code.md.- A
PostRuntimehook can no longer add or change writes.pendingProposals,effectsandcompletionof the replaced result always come from the runtime, and a result whosestatusa hook changed carries no writes. APostToolUsehook replaces only what the model (orctx.tools.call) reads; the tool's writes, events, success and approval status stay. UsePreStateCommitto stop a write. Rules:docs/reference/hooks.md. - A misspelled
DEPLOYMENT_TIERstops the server at startup. A value other thanself,demoorcommercial(case and surrounding spaces do not count) was read ascommercial; the server now refuses to start and the error lists the accepted values. A hosted tier also refuses to start withCOVEL_ALLOWED_HOSTS(the Host check runs only onself), andcommercialwithCOVEL_MEDIA_CLEANUP_ENABLED(the cleanup endpoint stays closed there). Remove the setting or correct the tier. Rules:docs/internals/security.md#host-profile. - FilterContainer rows now use
props.itemTemplate. ReplaceitemComponent,itemPropMapanditemLiteralPropswith a catalog subtree and$itembindings. Binditemsto a state array with$stateor$item;$index, editable fields and nested repeats keep their original array positions after filtering. Existing session data does not need recreation. - The unused
contributes.ui.leftplacement is removed. Userightormessage. Loading ignores unsupported placements with a warning;validate:pluginreports an unknown field. UI discovery and API responses no longer advertise or returnleft. - Data schema version 5: an earlier SQLite or PostgreSQL database is refused at startup.
turn_messageshas a newwrite_seqcolumn, and the newsession_write_countersandrecord_changestables are filled by triggers. A database recorded at data schema 4 stops the server at startup withStoreSchemaVersionError("written by an older Covel (data schema 4; this version uses 5)"), before any table is created or changed; one from before the version was recorded is refused for the missingturn_messages.write_seqcolumn. Back up the SQLite file with its-waland-shmfiles and start with a new data directory orSQLITE_PATH, or pointDATABASE_URLat a new, empty PostgreSQL database. Browser-private saves need nothing. Seedocs/internals/storage.md. readTurnMessagescursors are save positions. A cursor a plugin stored from an earlier version is rejected withInvalid turn message cursor; read from the start again (afteromitted). Pages now come in the order messages were saved, which is the time order except for background output saved after a later turn.- Store contract changes for a custom
DataStore.listTurnMessagesAfter(sessionId, after, limit)takes the lastwriteSeqread (ornull) and returns messages with theirwriteSeq. New:listRecordChanges(the change feed of lorebook entries, characters and plugin data, deletions included),getCharacter, and on the vector capabilitygetVectorPayloads. - A world has one character file. The roleplay notes of a character (persona, voice, secrets, rules, example lines, relationships) are written in its record in
characters/characters.json, underblueprint, and no longer in a separatecharacters/main-cast.json. The notes do not repeat the record's name, aliases, description, type or fields: anameor similar field inblueprintfailspnpm validate:world. Relationship keys are character ids (npc-su-yao, notsu-yao).main-cast.jsonis no longer read; move each card's notes into the record of its character. The four bundled worlds are converted. Rules:docs/reference/world-data.md#one-character-file-per-world. - Character Profiles matches a character by its id only.
character-blueprint(0.0.37) stores the notes keyed by the character's id and no longer matchesnpc-<card id>, for the notes and for portraits alike. The read-only "Preset Characters" panel is gone: the characters are in the character list. Development sessions of Emberback, Haruka Academy, Lantern Barrow and Mistport created before this change keep the old notes keyed by card id, which the narrative no longer reads; recreate them. - The world generator writes characters as character records. A generated character has
id,name,type,description,aliasesandfields;attributes,role,instantiate,schemaVersionand loose persona fields are dropped. Roleplay notes, when requested, go into each character'sblueprint. io.visibility: pluginis gone. A runtime isstory(the narrative provider) orsystem(every other runtime), andsystemis the default, so deletevisibility: pluginandvisibility: system. A package that still saysplugindoes not load; the error names the file and the field.RuntimeManifest.outputKind, the hook payloads andGET /api/framework/capabilitieslist onlystoryandsystem. The plugin list no longer shows the "Auxiliary" badge. The defaults are listed under "Defaults" indocs/en/reference/plugins.md.- The
scheduledtrigger is gone; anautoruntime takesschedule.every. Replacetrigger: { type: scheduled, interval: N }withschedule.every: Nand droptrigger.type: auto, which is the default.startTurn,cooldownTurnsandmaxTriggerCountstay underschedule.trigger. A package that still saysscheduledorintervaldoes not load; the error names the file and the field. The runtime summaries ofGET /api/pluginsand the plugin flow carrytrigger.everyinstead oftrigger.interval. - An agent that leaves out its model slot or loop limits gets new defaults.
agent.modeldefaults tostoryfor astoryruntime andpluginfor any other, where it used to fall back to the request'sdefaultbinding.agent.loopdefaults totimeoutMs120000,callTimeoutMs60000,maxRetries3 (astoryruntime: 240000, 120000, 1), wheremaxRetriesused to default to 1. Write the old values if a runtime needs them. - Every bundled plugin whose manifest changed has a new version. Setup runtimes of existing development sessions run again at the next player action; recreate a session if its setup guard does not skip work already done.
- A runtime no longer writes
type. A runtime with afunctionblock is a function runtime; one without is an agent. Deletetype: agentandtype: function; a function runtime that sets nothing else writesfunction: {}.function.handlerdefaults to./handler.jsnext to the manifest; a function runtime with neither a written handler nor that file does not run, andpnpm validate:pluginand installation report it. An installed package that still writestypeloads with the field left out and a warning, andpnpm validate:pluginreports it as an error. Rules: "Two kinds of runtime" indocs/en/reference/plugins.md. - The root
optionallist is gone. A contract that a runtime reads throughio.inputsorschedule.needsbut thatrequiresdoes not list is optional: it binds when a provider is active, and a required input without a provider keeps the runtime from running. Deleteoptional:; a package that still writes it loads with the field left out and a warning, andpnpm validate:pluginreports it.GET /api/plugins/:idstill returnsoptional, now worked out from the runtimes. contributes.data.*.authoring.source.kindis gone. The format comes frompath:.yaml/.yml,.json, or a media directory without an extension. Deletekind, andkey: id(key: filenamefor a media directory), which is the default. A package that still writeskindloads with the field left out and a warning.package.jsonis optional, and itsversionis not read. The version lives only inPLUGIN.md; the check that the two versions match is gone, and the install preview and update check readPLUGIN.md. A package withoutpackage.jsonloads its.jsfiles as ES modules; one that has apackage.jsonstill declares"type": "module". An installed package keeps working; deleteversionfrom itspackage.json.- The installer names a package that writes no
idafter its folder. The ID is the writtenid, else the top-level folder of the ZIP, the selected repository folder or the repository name, else thepackage.jsonname without its scope andplugin-prefix. Apackage.jsonname no longer has to match the writtenid. Order:docs/en/reference/plugin-installation.md. - Every bundled plugin has a new version. Setup runtimes of existing development sessions run again at the next player action; recreate a session if its setup guard does not skip work already done.
- The settings of an agent sit directly under
agent.agent.loop.completionmoves toagent.completion,agent.llm.toolChoicetoagent.toolChoice, andagent.llm.reasoningEffort: disabledbecomesagent.reasoning: false;agent.llmis gone andagent.loopkeeps only the time, retry and step limits. Move the three fields up one level. Rules: "Completion conditions" and "Tool choice and reasoning" indocs/en/reference/plugins.md. schedule.completionis nowschedule.detached. Writedetached: truein place ofcompletion: { mode: detached }, anddetached: { settle: true, maxSettleWaitMs: … }in place ofsettle: before-next-execution;maxQueueMsandmaxExecutionMsstay.overlapandstalePolicyare gone: each had one allowed value, and detached jobs of a runtime still run one at a time and a stale job is still rejected.GET /api/pluginsandGET /api/plugins/:idreturnturnCompletion.settleas a boolean and no longer returnoverlaporstalePolicy. Jobs already queued keep working.schedule.manual.executionis nowschedule.background. Writebackground: truein place ofmanual: { execution: background }, and deletemanual: { execution: sync }.- A manifest that still writes a removed nested field does not load.
agent.llm,agent.loop.completion,schedule.completionandschedule.manualare unknown keys inside fields the host reads, so loading, installation andpnpm validate:pluginreject the manifest and name the file and field. - Every bundled plugin whose manifest changed has a new version (affinity, branch-reply, char-creator, character-blueprint, chat-mode-narrator, codex, dice-check, guide, history-compaction, memory, narrator, npc-graph, scene-stage, soundtrack, story-events, world-init, world-ir, world-time). Setup runtimes of existing development sessions run again at the next player action; recreate a session if its setup guard does not skip work already done.
guideandworld-irnow ask the model to call a tool on every step instead of naming their one tool, which is the only tool each of them sees. - A world's
pluginPolicyis one built-in pack plus a few plugins. Writepreset(a pack ofpacks/builtin.yaml),plus(the plugins the world also wants), and optionallypitch(thelabel,descriptionandreasonthe session-prep screen shows for that pack),packs(the IDs of the packs the player may pick) andrequires.presetId,requested,recommended,preferredTagsandavoidedTagsare ignored with amanifest-unknown-fieldwarning, andpackswritten as objects (packs a world defined itself) make the wholepluginPolicyinvalid, so such a world starts with no plugin selection. Rewrite the policy of every world you wrote; worlds inCOVEL_USER_WORLDS_DIRmade by an earlier AI world creation must be generated again or edited by hand.kind: coreplugins are never listed: every session has them. Reference:docs/reference/world-data.md. - The languages of a world come from its files.
supportedLocalesis no longer a field ofworld.yaml: a world has an edition in each language that has aworld.<locale>.yamlor aWORLD.<locale>.mdat its root, plusdefaultLocale. Delete the field. Aworld.<locale>.yamlorWORLD.<locale>.mdnow makes an edition even when the field did not list its language. dimensionSourcesis removed. Declare dimensions inline inworld.yamlor as the whole table indata/dimensions.yaml.- A descriptor only writes what differs from the conventions. Every world package is read by convention, with or without
data/world.data.yaml. An entry whosepathis a conventional source's path changes only the fields it writes (enabled: falseturns the source off); any other entry adds a source. A descriptor that listed every conventional file still loads, but a conventional file it left out is now imported: turn it off withenabled: false, and delete the entries that repeat the conventions. Conventional sources have new IDs (for exampleblueprintsinstead ofcast,rules-campus-lifeinstead ofcampusRules), so an override descriptor that names a source of a bundled world must use the new ID, and sessions of the bundled worlds created before this change should be created again. - A plugin can no longer bring its own model or endpoint. Before, an
llm.tomlinside a plugin's directory could set a provider, model,baseUrlandprotocolfor a role ([plugin.<role>]); the server used it in place of the player's choice, and the player's request keys followed it to the plugin's address. The file is no longer read; a plugin that shipped one now uses the player's model for that role. A plugin names only a role withagent.model/function.model; the player's (or operator's) model settings decide the model and where requests go.pnpm validate:pluginwarns when a plugin still ships anllm.toml. Rules:docs/reference/slots.md. ctx.recursiveCallrejects targets outside the caller's own plugin package. Third-party plugins that named another package's runtime must use services for requests and responses, or events to trigger follow-up work. The error identifies the target runtime and caller package before nested execution starts. Bundled and community plugins follow the same rule; see nested calls.- A suspension records what it was taken under. Each suspension now saves the number of player turns in the history and its plugin's version (
pendingContinuation.takenUnder). Development suspensions and browser checkpoints that hold a suspension from an earlier build must be recreated. - Data schema version 6: a database written at data schema 5 is refused at startup. The
eventstable drops its unusedtarget_runtimecolumn. A SQLite or PostgreSQL database recorded at data schema 5 stops the server withStoreSchemaVersionErrorbefore any table is changed. Back up the SQLite file with its-waland-shmfiles and start with a new data directory orSQLITE_PATH, or pointDATABASE_URLat a new, empty PostgreSQL database. Browser-private saves need nothing. Seedocs/internals/storage.md. - UI render blocks require
parts. The{ type, ...content }single-block shape is no longer accepted. Tools and handlers must send{ parts: [...] }; recreate development sessions containing uncommitted effects in the old shape. Seedocs/reference/tools.md. - Form fields require
name. The submission handler no longer acceptsidas an alias. Update field definitions and recreate development sessions with pending forms that use onlyid. - Session events use one current payload shape.
ui.renderedalways includes the committedrenderandblock;narrative.delta,narrative.completed, andruntime.startedrequirekind. Custom event producers must send these fields. Clients no longer rebuild UI blocks or infer a kind from runtime labels. See the session protocol reference. POST /api/ai/generate-worldtakesconceptonly. Clients must sendconceptinstead of the removedpromptsynonym. Seedocs/reference/api.md.- Generated world records no longer store
metadata.generatedPackageSummary. Clients must derive counts frommetadata.embeddedCharacters,metadata.embeddedLorebookor the world package content. create-formnumber and checkbox defaults must be numbers and booleans. String defaults such as"5"and"true"are rejected. Plugin authors must update those fields to5andtrue; recreate affected development forms. Seedocs/reference/tools.md.
Added
- Root
permissions.httpinPLUGIN.md. A package whose entry code calls an outside service declares the origin there. Seedocs/reference/plugins.md. - The game screen says which world data is missing. The create-session response lists the world data that was not imported (
worldDataNotices), and the game screen shows it, with a note that the data arrives when the plugin is turned on. Enabling a plugin returns what it imported (worldData), and the notice is updated. - A "Narrative Only" gameplay pack. It turns on the narrator and World Dimensions and turns the other always-on plugins off: no opening form, character creation, world time, Story Memory or history compaction. It starts every bundled world.
- Gameplay packs can turn plugins off. A pack's
excludedlists plugins,kind: coreones included, that choosing the pack turns off; the player can turn them back on, and the next pack lifts only the exclusions this one added. Reference:docs/reference/world-data.md. - "Back to the minimal set" on the session prep screen. One click keeps only the narrative plugins, the plugin that holds the world's dimensions and what the world requires, and turns everything else off.
- Self-hosters get a SQLite Docker stack, and it is the default.
docker/docker-compose.sqlite.ymlruns one app container with no database service. The SQLite file, its media files and the installed worlds and plugins live in a named volume (SQLITE_PATH=/home/node/.covel/data/covel.db), so they survive recreating the container. It keeps the loopback binding andDEPLOYMENT_TIER=self, and it refuses to start withoutCOVEL_MEDIA_TOKEN_SECRET. Run it withpnpm docker:build; details indocs/reference/env-registry.md#docker-compose. - Every plugin can import the plugin SDK and
zodwithout shipping them. The host resolves@covel/plugin-handlers-utils(with its/dimensionsand/promptssubpaths) andzodto its own copies for the entry, handlers, guards and world projections of every plugin, bundled or installed. A plugin copied out of the repository loads as it does inside it; any other library it uses must still be bundled.docs/plugins/essentials.md,docs/ship/package.md. pnpm validate:worldchecks the dimensions that plugin data names.contributes.data.<ns>.authoring.referencesacceptsto: world-dimension, and a referencepathcan write**.before a field to find it at any depth (when.**.dimension). A value that is not a dimension the world declares, or a path beside it (within) that the dimension's schema does not allow, is adimension-referenceerror. story-events declares the dimensions in its event conditions, so a misspelled dimension ID or path, which made an event never fire, is reported before a session starts. Documented indocs/reference/plugins.mdanddocs/reference/world-data.md.- A session tells the player when its memory index was built with another embedding model, and rebuilds it in one click. Before, changing the embedding model paused vector memory in older sessions until the old model settings were restored, retained the old vectors, and logged a warning after every turn. Now the session shows "Memory search is matching keywords only" with a "Rebuild index" button ("Clear index" when no embedding model is configured). The rebuild measures the new model first and changes nothing when its configuration is invalid or it does not answer; then one transaction deletes the session's old vectors and index progress and locks it to the new model, and the index is built again in the background. Endpoints:
GET /api/sessions/:id/memory-indexandPOST /api/sessions/:id/memory-index/rebuild(docs/reference/api.md). - The plugin SDK types everything the host passes to a function handler or guard.
PluginFunctionContextin@covel/plugin-handlers-utilsnow hasexecution,recursionDepth,recursiveCall,session,activation,gateway,utils, and the media capabilitiesmedia,images,speech,musicandassetProgress, with their types (PluginImages,PluginMediaRef, …) andPluginTurnDigestfor theturn-digest@1kernel input. The host builds its context from these types, and a compile-time check plus a runtime test fail when the two drift apart. - "Restore the package version" brings an edited world back to its files. The world's detail page shows that a package world was edited and offers the package version back (
POST /api/worlds/:id/restore-package); after that the record follows its files again. Documented indocs/reference/api.md. - Filtered rows can combine layouts, buttons and badges. Their templates use the normal json-render scope and action handlers, with shared static and runtime validation.
$mapdisplays translatable labels for data values. Translate labels and string fallbacks through plugin locale catalogs. Character trait type and category badges now use these labels.- The player is told which plugins are off and why. The session prep screen lists the plugins a selection will turn off, what each one needs and which installed plugin provides it, with a button to turn that plugin on. The game screen does the same for the session's plugins that cannot run, in the player's language.
- An explanation when no plugin tells the story. When none of the enabled plugins writes narration, the session prep screen and the game screen say that turns will not tell a story and offer the installed narrative plugins, or point to Settings → Install & manage when none is installed.
- A plugin can take its data from the world's character records.
contributes.data.<namespace>.authoring.characterFieldnames a field of a character record (in place ofsource); the kernel imports the record as a character either way and gives that field to the active plugins that accept the contract, keyed by the character's id. Without such a plugin the field is ignored.pnpm validate:worldchecks the field against the receiver's schema, warns when no plugin reads it or a new session does not enable one, and locale files translate it like the rest of the record.pnpm describe:authoringandGET /api/framework/authoringreportcharacterField. Reference:docs/reference/plugins.md. - A world kept in a store imports its characters like a package.
metadata.embeddedCharactersgoes through the same import ascharacters/characters.json, so plugin fields in those records reach their plugins and the import ledger records each character. pnpm validate:pluginwarns about a field written with its default value. It names the file and field (is the default value; remove it) and the package still passes. It also warns when a package with anentrymodule or a setup runtime has noversion, since setup reruns only when the version changes.agent.finishWith: <tool>ends an agent after one tool. The run completes once that tool succeeds and fails without it, and the model must call a tool on every step unlessagent.toolChoicesays otherwise. It replacescompletion: { require: tool-use, afterTools: [<tool>] }withtoolChoice: required; the fullcompletionstays for several tools orruntime-done, and the two cannot be written together.- Hooks and extensions have a one-line form.
contributes.hooks: [PreToolUse]lists hook events by name, andcontributes.extensions: [prompt.segment@1/notes]writes an extension as<point>/<id>. The object form stays forenforce,order,slot,watchandpreview. pnpm validate:pluginwarns about more fields written with their default or in a long form. New defaults:schedule.detached: false,schedule.detached.settle: false,schedule.background: false,agent.reasoning: true,agent.toolChoice: requiredbesidefinishWith, andcontributes.extensions[].order: 0. A hook with onlyevent, an extension with onlypointandid, and the long form offinishWitheach get a warning that names the shorter form. Table: "Defaults" indocs/en/reference/plugins.md.- The smallest world is
name,summary,defaultLocaleandWORLD.md.idis optional: a world takes the name of its folder, or of its ZIP file when installed from one.docs/worlds/first-world.mdstarts from this world. - Rule files go in
data/rules/*.yaml. A plugin'sauthoring.source.pathcan end in*.<extension>: each such file of the folder is a conventional source, imported in file-name order, so a world adds a set of rules by adding a file. pnpm validate:worldreports values that need not be written. Adefault-valuewarning names acharacterSchema.typesequal to the default[npc, companion]and akind: coreplugin inpluginPolicy.plus;unknown-pack,preset-not-offeredandpitch-without-presetcheck the new policy fields.- Plugin lists show partial-load warnings. Session prep, the session plugin list, and installed plugins in Settings identify each unavailable runtime and ignored manifest field, with expandable reasons. Plugin API responses carry the same diagnostics with package-relative paths, and reloading a corrected plugin clears its warnings.
- A notice for plugins a save cannot run. When a confirmed plugin is not installed, failed to load, or its installed version lacks a provider or conflicts with another member, the game screen names it and play goes on without it.
GET /api/sessions/:id/pluginslists them inunavailable. Such a plugin is off everywhere: no runtime, hook, service, action, form validator, panel or memory search reaches it, and a direct call to one of its actions returns404 plugin_not_activewith the reason. - The showcase website tells a visitor how to open the unsigned app. Under the download button there is now a note for the visitor's system. On macOS, which reports the build as "damaged" after a browser download, it gives the command that removes the quarantine attribute:
xattr -dr com.apple.quarantine /Applications/Covel.app. On Windows, where SmartScreen shows "Windows protected your PC", it names the two clicks: "More info", then "Run anyway". pnpm validate:worldchecks the character attributes that plugin data names. A plugin declares incontributes.data.<namespace>.authoring.referenceswhich values of its records are theidof an attribute of the world'scharacterSchema(path,to: character-attribute, optionaltypeandwithin). A value that names no attribute, one of the wrong type, or a number outside the attribute'sminandmaxis anattribute-referenceerror with the file and the place in it.tabletop-rulesdeclares it for its point-buy rules, so a mistyped attributeidindata/tabletop-rules.jsonno longer waits until a session starts to fail. Rules:docs/reference/plugins.md,docs/reference/world-data.md.pnpm validate:worldwarns about a field that no model reads. A field of a world-data record or a character whose name ends inAtor istimestamp, with an ISO time as its value, is left out of what a model reads as bookkeeping. Each one now gives amodel-hidden-fieldwarning that names the file, the record and the field. Rename a date of the story (date,diedOn) or write it in another form to keep it visible. Anid, or a field whose name ends inId/Ids, that holds a UUID is left out the same way and gets the same warning, with the hint to use a readable ID such asnpc-lin-yao.- Worlds show when their package has changed. Bundled and installed worlds display an "Update available" badge when files on disk differ from the imported version. "Restore the package version" applies the package’s world data and settings while preserving saved sessions.
- Magpie is a built-in local provider. Pick it under "Provider" when adding a provider: it fills in
http://127.0.0.1:3425/v1with OpenAI Chat and needs no API key. A model can switch to OpenAI Responses or Anthropic Messages under its "API protocol". See the slot reference.
Changed
- A normal shutdown puts unfinished background jobs back in the queue. A job that had not started its commit used to end as
cancelled(worker-shutdown) and needed a retry by hand. It now goes back toqueuedwith the same job ID once its run has stopped, and runs again after the restart; a job already committing finishes its commit. What the stopped run did outside (a provider call) is done again, so a background job must be safe to rerun. A crash still ends inorphaned. Details:docs/reference/protocol.md. - Model lookup prices live only in
capability.pricing.GET /api/model-db/lookupno longer duplicates token prices directly undercapability; clients must read the nested fields. No stored model data needs to be recreated. - More natural English in Mistport and Haruka Academy, and plainer hints in the app. The English editions of the two Chinese worlds read less like a translation and use the names their world guides use. The English guide of Haruka Academy has its whole opening scene again, and a draft leftover in its opening scenario is gone. Longer hints and descriptions in the settings, session and theme screens say what happens in plain words.
- Plainer wording in a few places. The Chinese guide of the Emberback world reads as Chinese prose instead of a close translation, the onboarding guide no longer asks a question in a heading and names the labels the game shows during a turn, and the first-session guide and the Mistport pack description lose a piece of jargon each.
- Covel refuses a database written by another data schema instead of failing later. SQLite records the data schema version in
PRAGMA user_versionand PostgreSQL incovel_schema_meta. A database recorded with another version, or an older one that lacks a column, is refused at startup before any table is touched, withStoreSchemaVersionErrortelling you to back up and use a new data directory; the desktop app shows this on its startup screen. Before, such a database opened and then failed withno such columnon use. Rule:docs/internals/storage.md#capabilities-and-migrations. - Desktop builds use Electron 44.6.0. The runtime and packaging tools receive stable fixes; the macOS 13 minimum is unchanged.
- Source toolchains pin pnpm 12.10.1 and Node 26.10.0. Mise, CI and the Docker image pin Node 26.10.0, the newest release eligible for the seven-day release window at this update. Dependency installation retains that waiting period without an Oxlint exception.
- Switching the desktop data directory can bring your data along. After you pick a new directory in Settings → Desktop, Covel asks whether to copy your existing data or start empty. With "Copy my data", the next start copies the database files and
worlds/to the new location before the backend opens the database, and switchesdata_rootonly after the copy succeeded; a dialog reports the result. Nothing in the old directory is moved or deleted, nothing in the new one is overwritten (a directory that already holds Covel data is used as it is), and a failed copy is removed while the app keeps using the old directory. Editingconfig.tomlby hand andPUT /api/config/data-rootstill copy nothing; the settings page now says where the old data stays. Rules:docs/reference/desktop-config.md. pnpm docker:*now runs the SQLite stack; PostgreSQL moved topnpm docker:pg:*.docker:up,docker:build,docker:down,docker:down-allanddocker:logsusedocker/docker-compose.sqlite.yml. The PostgreSQL stack indocker/docker-compose.ymlis unchanged and has the same five commands asdocker:pg:up,docker:pg:build,docker:pg:down,docker:pg:down-allanddocker:pg:logs..env.examplehas the PostgreSQL variables commented out. SQLite needs none of them. Uncomment the "PostgreSQL (optional)" block beforepnpm db:up,pnpm dev:pg,pnpm test:pgorpnpm docker:pg:build.- Plainer wording in the world, recipe, shipping and plugin guides. A few sentences in these chapters of the documentation read more naturally, in Chinese and in English; no instruction or example changed.
- The Chinese edition of Emberback reads as Chinese. Its characters, dimensions, items, quests, rules, lorebook and memory blocks were rewritten from close translation into plain Chinese, with the names the Chinese world guide uses. A leftover English fragment in a character's fears is gone, and dice checks use the glossary's Chinese term everywhere.
- A runtime has one waiting suspension. When a runtime suspends again, its older suspension is voided in the same transaction, settled like an abandon (its turn may count and release held-back jobs). The resume-time
supersededreason ofsuspension_outdatedis gone, and the web client drops the older item when the newturn.suspendedarrives. - Bundled plugins declare the SDK and
zodonly as development dependencies. Theirpackage.jsonfiles no longer list runtimedependencies, so each bundled plugin passes the same package check the installer applies to a ZIP, GitHub or collection install. Plugin authors list these two packages underdevDependenciestoo:docs/ship/publish.md. - A changed embedding model is logged once per session. Ingestion notes the mismatch once instead of after every turn, and memory search falls back to keywords without a warning each time. An index write now names the model it was embedded with, so vectors from the model an index was moved away from are refused (
docs/reference/slots.md). - AI world creation offers every gameplay pack. The "Player Experience" choices are now the packs of the server's catalogue (
packs/builtin.yaml), with their own names and descriptions, so "Low Cost" and any pack added later can be chosen too; before, only two fixed choices existed. A pack'sdefaultViewModeis data of the pack: a world generated for "Dialogue Mode" opens on the stage because that pack says so, not because the generator knows its ID.GET /api/framework/authoringlists the catalogue in a newpacksfield, andpnpm describe:authoringprints it. - Plugin code picks instructions by the same rule as prompt bodies.
pickLocaleTextnow chooses withinstructionLocaleFor, soCOVEL_INSTRUCTION_LOCALEalso switches the instructions that bundled plugins write in code (dice checks, story events, scene cast, memory, NPC relationships, …), and every Simplified Chinese tag such aszh-Hans-CNorzh-SGreads the Chinese ones. Before, forcingenstill left Chinese blocks in a Chinese session. - A plugin's own prompt templates exist only in English and Simplified Chinese.
createPromptLoaderfrom@covel/plugin-handlers-utils/promptsreads<name>.mdand, for a Chinese session,<name>.zh.md; other language files are not read andpnpm check:promptsreports them. History summaries in a Russian session now get English instructions and are still written in Russian. The framework's ownprompts/keep their lookup by any locale. - An unknown manifest field no longer stops a plugin. When
PLUGIN.md, aRUNTIME.mdor the inlineruntimehas a field this version does not know, such as an old field that was removed or one written for a newer Covel, the plugin loads and installs without that field and the server log names the file and the field.pnpm validate:pluginstill reports it as an error. An unknown key inside a known field (permissions.fs,io.conceal) and a misspelling of a field that limits or identifies the package (permision,gaurd,covle) still fail the load. Seedocs/reference/plugins.md. - One copy of the manifest rules. Loading,
pnpm validate:pluginand installation check RUNTIME.md and the inlineruntimewith the same schema, and UI specs with the same rules, so a package that passes validation is not dropped or stripped in the game. A manual or event runtime withschedule.manual.execution: backgroundthat reads a same-execution input, another plugin'spluginIdin a UI spec, a newerspecVersion, a webview entry that is not a local.htmlof at most 512 KiB, and an unknownsurfacesvalue are now reported by validation instead of failing in the game. - A runtime that can never run no longer hides itself. An
autoruntime withoutschedule.stage, or a background manual/event runtime that reads a same-execution input, is left out at load with the file, field and reason in the server log; the rest of its package loads, and its output contract no longer counts as provided. Validation and installation report it as an error. - One broken UI spec hides only itself. A spec that cannot be read or breaks a rule is reported in
/api/ui-specsdiagnostics[]; the plugin's other panels still show. - An empty account or a refused key is no longer retried at the same provider, and now switches to a configured backup. A 429
insufficient_quota, a 402, a 401 / 403, or a 400 that says the balance is used up is sent once: the HTTP layer and the runtime no longer resend it, and a fallback model the player configured takes over (before, a 429 quota error was resent up to three times and a 402 or 401 never reached the backup). The kinds areProviderFailureKindandisAccountFailurein@covel/shared. Rules:docs/reference/slots.md. - A world starts even when something it relies on is missing. A contract the world requires (
pluginPolicy.requires) with no provider, or with several and none chosen, no longer stops session creation with400 world_requirement_unmet: the session starts without that part, the session-prep screen says what is missing instead of turning the start button off, and the response lists it inunmetRequirements. Documented indocs/reference/api.mdanddocs/reference/world-data.md. - A plugin action has 30 seconds. An action handler runs inside the session lock; one that does not finish in 30 seconds now fails with
504 handler_timeoutand releases the session after already-started store calls settle, instead of holding every request for the session (deleting it included) until the server restarts. The handler getsctx.signal, which aborts at the limit, and itsctx.storerefuses every call after the action ends. Writes made before the limit are kept. Details:docs/reference/api.md. - The world record is the world; a package's files are only what it was installed from. New sessions, the world-data preflight and
sync-data/sync-dimensionsnow use the dimensions and the character schema of the world record, so dimensions saved in the world editor or imported on the session-prep screen reach new sessions and syncs for bundled, installed and generated package worlds too (before, a world with a package read them from its files and the edit had no effect). The characters, lorebook entries, plugin data and media underdata/andmedia/are still read from the package. Rules:docs/reference/world-data.md("World records and world packages"). - A generated world keeps its characters under one key. The record of a world the generator made no longer repeats its characters in
metadata.characterBlueprints; they are inmetadata.embeddedCharacters, as for every other world. Worlds made by earlier builds already have both keys and keep working. - The deployment form is decided once, at startup. The server combines the tier, the storage it runs on and the tokens into a host profile (
apps/server/src/host-profile.ts); route guards, media routes, server settings, a request's own model settings and trace retention ask it instead of reading environment variables.@covel/ai-providerreceives its local-network and community-wire rules from the host (configureProviderHostPolicy) and no longer readsDEPLOYMENT_TIER. The desktop app always starts its server withDEPLOYMENT_TIER=self. A test lists every API route with the host actions it needs. POST /api/llm-config/reloadrefuses a missing desktop token withdesktop_rest_token_invalid, the same code as the other desktop config routes (it wasunauthorized; the status stays401).- One prefix for every temporary media reference. World-data imports now pin their bytes as
media-pin:<uuid>instead ofworld-data-import:<uuid>, andPOST /api/media/cleanupexpiresmedia-pin:references older than 24 hours. Aworld-data-import:reference left in development data no longer expires; recreate that data or remove the reference by hand. - A missing dependency turns its dependents off instead of refusing the game. Turning off a plugin others need (say Pre-Game Setup, which Character Creator needs) no longer makes session creation fail with a contract name. The plugins that need it are turned off, the game starts, and they come back once the dependency is turned on again. Creation now fails only when a plugin the player asked for conflicts with another one, with
400 plugin_rejectednaming the plugin. Rules:docs/reference/plugins.md(session plugin selection). - Bundled manifests and the plugin scaffold write only what cannot be defaulted. Defaults such as
cardinality: one,required: true,format: summary,enforce: normal,role: systemandcontributes.data.*.version: 1are left out, and theanalystscaffold no longer sets its own loop limits. - The loader works out the fields an author used to repeat.
idis the directory name,kindisplugin,entryis./server/index.jswhen that file exists,providestakes in every runtime'sio.output.contract, and theui:*andcost:*tags come from the package's panels, actions and runtimes (a written one is ignored; authors write only tags such asmode:*). A writtenidmust still equal the directory name.pnpm validate:pluginwarns about a written value equal to the worked-out one. A plugin can now be onePLUGIN.mdwith a description, a stage and a prompt. Table: "Defaults" indocs/en/reference/plugins.md. - The default scaffold writes no
package.json, and bundled manifests drop the derived fields.pnpm create-pluginwritesversion: "0.1.0"inPLUGIN.mdonly;--with-toolskeeps a workspacepackage.jsonwithoutversion. The guidesfirst-plugin.mdandprompt-only.mdstart from the one-file plugin. - Docker Compose and CI use pgvector 0.8.7 on PostgreSQL 18 (
pgvector/pgvector:0.8.7-pg18, pinned by its multi-platform digest). - A plugin package must declare
"type": "module"in itspackage.json.pnpm validate:pluginand the ZIP and GitHub installers now refuse a package without it. Plugin packages were always ESM; without the line Node read the package's.jsfiles as CommonJS, so animportfailed and the development reload never picked up a change. The bundled plugins and both scaffolds already declare it. An existing package without the line needs it added. - The prep screen names plugin tags and summarizes world data in the player's words. The derived tags show as "Calls a model", "No model calls", "Side panel", "Message cards" and "Player actions" instead of
cost:llmorui:manual-action, in the filter chips, the group headings and the plugin badges, and a plugin row shows all its tags. The world-data section counts what will be imported (characters, lore entries, images, plugin records) instead of listing storage addresses. - Turning one plugin on or off says what else moved. When the switch also turned on a provider another plugin needs, or turned off a plugin that conflicts with the one chosen, a short notice names them and says why.
- The prep screen survives a reload of the tab. The world being prepared is remembered for the tab; opening the app anew still starts on the world list.
- World-data notices carry a code.
worldDataNoticesitems ofPOST /api/sessionsand the import after a plugin is turned on have an optionalcode(dimension-provider-missing,receiver-missing); the game screen says those in the interface language and folds any other English message under "Details".docs/reference/api.md. - A refused toggle names the provider that must stay on. The
409 dimension_provider_requiredresponse hasdetails.pluginIdanddetails.displayName, and the toast names that plugin. A refusal the app knows no longer prints the raw response under the toast. - A save keeps the plugins the player confirmed. The plugin list, the slash commands and the executor now read one set: the session's
activePlugins, checked against the installed versions. A plugin installed later, acoreone included, no longer joins an old save, and the list no longer shows a plugin as enabled when it does not run (or offers a command that then answerscommand_not_active). A development hot reload no longer rewritesactivePluginseither. To recompute the set from the installed plugins, enable or disable a plugin. Rules:docs/reference/plugins.md(session plugin selection). - Inside
withTransaction, write only throughtx. ADataStorewrite made through the root store from inside the callback now throwsRootStoreWriteInTransactionErroron every backend and writes nothing; it used to join the transaction on SQLite and commit on its own on PostgreSQL. Plugin authors are not affected: plugins never see the store. - An out-of-date suspension is withdrawn instead of resumed. When the player has played another turn or the plugin changed version,
POST /api/sessions/:id/suspensions/:suspensionId/resumevoids the suspension and answers409withcode: "suspension_outdated"; the web client removes it and tells the player. Rules:docs/reference/api.md(Out-of-date suspended items). queryDimensionSnapshotbuilds its model text without bookkeeping. The_textit returns from@covel/plugin-handlers-utils/dimensionsnow selects paths and pages inside themodelFacingJsonprojection, asworld-dimension-getalready did, so a plugin tool that hands it to a model never shows a session ID, row UUID or write time.resultskeep the full values. Rule:world-dimension-get.- The Chinese edition of Emberback reads as natural Chinese. Every text in
worlds/emberbackwas translated again for meaning rather than word for word, and checked line by line against the English: 37 meaning errors are fixed (among them the starting location, the convoy states and the signal-log values the model must write). The five player attributes have plainer Chinese labels, and four names that were wrong in meaning are renamed: the Switchyard, the Long Blackout, Mirror Farm (no longer sharing a name with the Solar Fields) and June Okafor. Sessions created before this change keep the text they were created with. - Emberback's Chinese names are plainer and used one way throughout. The Chinese edition renames the place, the Duskline, the Crownfire and its front, the Nightglass observatory, the Nightward Shelf and the Condenser Yard; Dr. Mina Park, Priya Nair and Tomas Reed's title; the linekeeper, the crawler and Echo Reading with its two top ranks; the three history events; and the storm reader, trade chits, cryo-pack and field radio. Only the Chinese files change; the English edition, ids and keys are untouched. Sessions created before this change keep the names they were created with.
- Provider account failures explain which model uses share the problem. Setup, recovery and failed-task notices name the provider and list the session’s active model uses routed to it. The failed provider and model survive reloads; authentication recovery opens provider settings to fix the shared key.
- Starting without a model key offers a choice. Session prep names the providers and text uses missing credentials, with actions to add a key or start anyway. Built-in remote providers without a key fail before any network request and are not retried; local and custom endpoints can still work without keys. Missing-key notices distinguish an absent key from an invalid one.
- Setup and failed turns have one clear recovery notice. The notice explains the recorded cause, links to model settings when a key, permission, endpoint or rate limit needs attention, and keeps retry in one place. Header and composer labels no longer repeat the failure while recovery is visible; timeline summaries stay muted with expandable details. Setup execution records stay neutral after play starts or the page reloads. See Built-in App Features.
- API validation errors are more consistent. Lorebook writes and model list/ping requests report coded JSON-body errors; session execution/control and world-data sync report
session_not_foundfor missing sessions. Plugin RPC field errors and session plugin mutation errors use shared validation wording. Status codes and approval requirements are unchanged. See HTTP API. - The English editions of Harukaze Academy, Mistport and Lantern Barrow read as natural English. Every English text of the three Chinese worlds was translated again for meaning rather than word for word and checked line by line against the Chinese by a second reviewer. About 60 meaning errors are fixed, among them lines that said the opposite of the source (a tide stage labelled "Receded", a lesson "too cheap to call a monopoly"), a student committee chair called "Head Librarian", and a torch that burned "about an hour" where the world's clock counts in phases. Characters have distinct voices, and some player-facing labels are plainer (Harukaze Academy's relationship stages are now Just met / Friends / Crush / On the verge / Together). Some names are clearer: Harukaze Academy, Mistport: The Riftide Chronicle, Captain Grey, Speaker Chen, the Tide-Seal Decree, the Tidewielder, the deep ebb, President Haruka Saegusa, the Newspaper Club and its Fact-or-Rumor Wall, and the Evening Breeze Lanterns. Sessions created before this change keep the text they were created with.
- Lantern Barrow's English edition has plainer names. The world is now "Lantern Barrow: The First Delve", the keeper is the Lampkeeper (Lampkeeper Ysolde, the Lampkeeper's Cottage) and the marsh-king is the Marsh King. Sessions created before this change keep the names they were created with.
Removed
- Unreachable bundled tool registrations are removed.
list-npc-graph,set-world-schema, andset-world-dimensionshad no runtime callers. World initialization keeps its private schema and proposal helpers behindinitialize-world. - The IndexedDB
MediaStoreand the@covel/store/indexeddbentry point. Nothing used them: browser-private play keeps its media on the server, and the web app's display cache is separate.MediaStore.delete()also loses itsopts.forceparameter, which no backend read;delete(id)always removes the asset with every claim on it. - Removed the unused package import entry for the private runtime test harness. Plugin authors continue to run cases with
pnpm test:runtime. POST /api/events/emitno longer takestargetRuntime. The field never chose a receiver: the event goes to the clients subscribed to the session and triggers no runtime. A request that still sends the field succeeds, and the field is ignored. See the API reference.- Compiled runtime manifests and
/api/plugin-flowsno longer exposepluginType. Package manifests, plugin summaries, and flow entries usekind(coreorplugin); runtime manifests carry no package category. Update clients that read the removed field. See the API reference.
Fixed
- Stop works before a turn starts and between the setup turn and the opening. A stop pressed while the action waited for the session or for the previous turn's memory update used to answer "no turn in progress"; one pressed after setup was saved answered OK and the opening still ran. A stop now applies to the player's whole action: an action that has not started does not start and saves nothing, and an opening that has not started does not start. The stopped opening shows as stopped and can be retried, and it then runs as the opening. Once an action has nothing left to stop,
POST /api/sessions/:id/abortreturns409. - A saved turn no longer ends in an error when its debug record fails. If writing the
turn.completedtrace (or announcing background jobs) failed after the turn was saved, the stream ended witherror.occurredand the opening was skipped. These steps are now only logged: the stream ends withexecution.completed, and the opening runs. - A resent message whose turn is already saved is refused at once. It used to run the whole turn, model calls included, before the commit refused it. It now ends with
error.occurredand the codeinput_already_committed, before any model call. - A stale retry says what happened. Retrying a task whose turn has moved on, or recovering a turn that can no longer be recovered, showed "Internal server error" in production. The stream now gives the reason with a code (
retry_source_stale,retry_target_not_failed,recovery_unavailable, …), and the app asks the player to refresh the task statuses. Codes:docs/reference/api.md. - The world art scripts no longer lose a paid picture when
cwebpis missing.scripts/generate-portraits.mjsandscripts/generate-scenes.mjsasked the image provider for a picture and only then found that the WebP encoder was not installed, so the picture was paid for and discarded. They now check forcwebpbefore the first request and stop with the install hint. - Deferred tool discovery describes both builtin and plugin tools. The
search-toolsdescription identifies the calling plugin without attributing every tool to it. - Plugin diagnostics and debug commands use the shared locale resolver. Compatible Simplified Chinese locales such as
zh-SGnow receive Chinese messages. - The memory backend reports the session's embedding lock consistently. Locking and rebuilding an index update its model ID and lock time; clearing the index removes both, as on SQLite and PostgreSQL.
- Startup failures show recovery controls even when the splash loads slowly. The desktop shell waits for the splash to load before delivering the error.
- Connection tests and model lists use newly saved keys immediately. Replacing or deleting a desktop key updates the same live key map used by turns, while preserving external environment keys and their precedence (F-033).
- Stage plugin messages wait for the player to send.
sendMessagefills the stage input so the player can review and edit it (F-013). - Stage plugin panels share loaded definitions and offer retry on failure. A failed request shows a localized message without blocking play (F-066).
- Bundled worlds refresh even before a user worlds directory exists. Missing directories count as empty inventories.
- Failed dimension reads preserve the last complete world. Startup and hot reload reject incomplete dimension sources without sending a dimension-change notice.
- Dimension and gallery translations follow the declared locale names. Bare-language overlay files reported as ignored are no longer compiled into catalog or session data.
- A background job whose runtime suspends now fails instead of showing as succeeded. When a background manual run, or an event runtime it triggered, suspended, the job was marked
succeeded(or failed with a misleading reason) while the runtime waited for the player. The job is now recorded asfailedand commits nothing. - A background job no longer commits the results of a plugin version that was reloaded while it ran. In development a plugin reload could swap the plugin between the start of a job and its commit; the commit now checks the version again and the job ends as
stale. - Repaired tool arguments remain valid in subsequent model requests. History reflects JSON repairs and
PreToolUserewrites, including native provider continuations. Invalid arguments retain their original text and error result. Malformed-history rejections no longer trigger an unchanged retry (F-026). - Accepted tool results keep one terminal outcome. Recording and completion traces follow result acceptance; telemetry failures only warn and preserve proposals. Later cancellation still aborts the enclosing runtime (F-146). Sentinel checks include the tool name (F-147), and declared completion tools govern the tool-use gate (F-150).
- World translation and generation keep the last good package on failed saves. File write-back uses staging and the ZIP-install package preflight, restores replaced files on record-save failure, and retains save-time conflict checks.
- One broken authoring contract no longer disables world creation. Unreadable examples and missing, invalid, or conflicting schemas skip only the affected contract, with diagnostics for authors and generation warnings.
- World generation, revision, and translation share their documented SSE exclusion. A second operation is rejected before model work begins, and admission is released after cleanup.
- Missing runtime manifests have consistent diagnostics. Put shared code in
lib/; every directory underruntimes/needsRUNTIME.md. Validation and installation report the offending path, while the loader warns and keeps the package’s other runtimes available. See the plugin reference. - A community plugin approved after the game starts gets the world's data. Session creation skips the world data that only a plugin waiting for approval receives. Enabling a plugin in the game now imports that data once for the plugins that just joined, and leaves the other plugins' data and anything the game changed alone. A community plugin's world projection runs then, with its approval. Reference:
docs/reference/world-data.md. - The "memory could not be condensed" notice now appears when compaction keeps failing. The count of failed attempts in a row was kept per request for requests that send their own model settings, so it restarted at 0 every turn and the notice never appeared. The count now lasts for the server process and is cleared when a session ends or is deleted. The notice no longer names a plugin; it asks the player to disable the plugin that condenses old turns.
- Submitted forms keep their saved values. Empty or missing answers no longer read a live form draft. Development sessions without saved answers must be recreated and submitted again to retain them.
- Plugin flow document links use discovery metadata. Missing paths stay empty instead of guessing a runtime document location.
- World validation and import catch invalid dimension condition values. Plugins declare candidate fields through
authoring.references.values; story events now check equality and membership candidates against dimension schemas before any import writes. - In-game plugin toggle failures explain the problem in the UI language. Known error codes have Chinese, English and Russian messages; unknown failures use a localized generic message, with the original server response retained in the details. Hyphenated plugin-resolution codes are preserved by the client.
- AI world creation and revision honor the player's model bindings. Authoring uses the
storyrole by default; API callers can select another role or preset. Every generated part, retry, and repair keeps the selected role and request-scoped provider keys. - Concealed runtime tool calls stay out of turn digests. Their status remains visible, but their arguments no longer reach digest consumers (F-044).
- Models no longer receive bookkeeping from character schemas, dimension queries or activation payloads. Full records remain available to code, and archival search omits UUID source keys (F-045, F-149, F-157).
- Model settings keep partial input until it is committed. Number fields and sliders use drafts, and consecutive saves no longer discard later edits (F-037).
- Model discovery follows the selected connection. Changing the target clears old results and list selections while preserving manually entered IDs (F-094).
- Destructive settings actions ask for confirmation. Provider and model deletion explain the affected keys and role bindings; plugin removal and theme deletion also require confirmation (F-191).
- Read-only settings retain recovery tools. Export, backup viewing and downloads remain available while setting writes stay disabled (F-199).
- World revisions can be cancelled while running. The request stays available for editing and retry, and late events from the cancelled request are ignored.
- Unsaved world dimension drafts are protected on cancel and reload. Discarding edits asks for confirmation, including incomplete form input and unapplied JSON. Successful saves clear the saved drafts' warning; failed saves keep it.
- Panel providers use the same naming rule in every group. Provider buttons show the plugin's localized name or ID, with no special treatment for
chat-mode. Browser world saves also stop interpreting the removedcharacterBlueprintSourcesmanifest field inside opaque metadata. - PostgreSQL servers that start together no longer fail on table creation. Startup table creation now runs under one advisory lock, so two servers booting against one database create the tables in turn.
- A game goes on when the plugin that keeps its dimensions is gone. When the plugin a session's dimensions belong to was uninstalled, failed to load or lost a dependency, every turn stopped, the session view failed, and the player could not turn any plugin on or off. Now the story goes on with the dimensions as they were saved, nothing changes them, and the game screen says they are paused until that plugin is back. Other plugins can be turned on and off meanwhile. Rules:
docs/reference/world-model.md. - Several plugins that keep world dimensions are explained, not a server error. Starting a game with more than one of them failed with "Conflicting dimension providers". Preflight and session creation now answer
409 dimension_provider_conflictwith the plugins indetails.candidates, and the prep screen asks the player to keep one of them. Turning on a second one during a game is refused with the same code, and a turn that meets several names them and says what to turn off. Codes:docs/reference/api.md. llm.toml.exampleno longer suggests a field the loader rejects. The commentedthinking = { type = "enabled" }line is nowreasoningEffort = "disabled";thinkingfails the load of the file.- Schema warnings no longer recommend a plugin tool the caller cannot use. Undeclared character fields remain a soft warning with the available attribute IDs.
- Unprefixed local runtime references receive the correct guidance. Validation suggests the complete package/runtime ID rather than a cross-package contract.
- World setup reuses authored schemas consistently and silently. Guard output now matches the published contract, accepts small authored schemas, and only adopts missing dimensions when setup reruns.
- Random world time follows the host random stream. Fixed seeds reproduce time steps across different session and turn IDs; repeated calls in one turn reuse the settled result.
- Session labels and errors follow the interface language. Canvas, storage, plugin failures, error badges and media jobs use translated text; punctuation and numbered labels are part of the translations.
- Image generation shares the global confirmation queue. Its authorization prompt no longer opens a separate dialog above another confirmation.
- Forms without an interaction submission target show an error. Their values remain editable instead of being sent as JSON chat text or marked submitted.
- Forks preserve the parent conversation’s message order. Messages written in the same millisecond keep their write order across repeated forks and browser checkpoint pagination, instead of being re-sorted by random IDs.
- Session preparation shows the world edition you will play. The world name and description follow the session content language throughout the preparation page (F-057).
- Failed save-list reads can be retried. The preparation page shows a loading state and a retryable error instead of making unavailable saves look absent (F-072).
- World data checks follow the latest plugin selection. Late preflight successes and failures no longer replace newer diagnostics (F-078).
- Long sessions keep bounded state-patch caches and asset progress. Only the latest field values are retained, and state panels still refresh for every new change (F-058).
- Loading older messages skips client-clock echoes when choosing the cursor. Recovered server messages replace local timestamps (F-063).
- Rejected input stays separate from later actions. Local echoes receive a turn only from their own request (F-064).
- Switching sessions no longer waits for another session’s action stream. Subscription pauses belong to the session running the action (F-059).
- Returning to a session reloads its UI slots without stale previews. Departure and host cleanup release the previous session’s slot cache (F-061).
- Recovery polling slows down during outages and pauses in hidden tabs. Failure retries back off to 30 seconds, reset after success, and avoid a second HTTP retry loop (F-065).
- Native EventSource reconnects resume missed events. The subscription stream accepts
Last-Event-IDas well as the existing query cursor, with the query parameter taking precedence. Stale header cursors receive the same reset response. - Keyboard and screen reader controls work throughout the session view. Resize handles show focus, notifications pause while focused, and portrait replacement, copy, and retry controls are available without hover. Session loading has a spoken status and the overflow controls use native button navigation.
- Queued confirmations start each request with focus on Cancel. Stage narration is readable separately from its advance button, with paragraph-level live announcements.
- Leaving the world editor right after choosing to keep editing asks again. A second attempt to leave, made before the editor had redrawn, was blocked without showing the discard dialog.
- Desktop “Copy my data” includes stored media. The copy stages
media/alongsideworlds/before placing the database. Reads, repairs, new writes and deletions use the current media root after relocation. Backup instructions now includemedia/. - Themes cannot probe saved API keys through CSS selectors or load resources from arbitrary hosts (F-009). Credential inputs no longer render saved values or reflect new drafts into HTML attributes. Page CSP restricts CSS images, fonts and styles in web and desktop, including development mode. See
docs/internals/security.md. - A resume no longer overwrites a write made after the suspension. When a background job, a manual run or any other commit changed a record the suspension would write, the resume answers
409 suspension_outdatedwithdetails.reason: "data-changed", voids the suspension and writes nothing; the record keeps the newer value. The check runs before the runtime and again inside the commit transaction. A write to another key does not block the resume. Rules:docs/reference/api.md(Out-of-date suspended items). - A suspended setup runtime no longer uses up its retries. Its attempt is recorded as suspended instead of successful, so a setup runtime with
maxTriggerCount: 1no longer becomesblockedas soon as it suspends. When the suspension is abandoned, expires or is voided, the attempt endsfailedand the runtime goes back topending(orblockedwhen the retries are used up), so it can be retried or skipped. - Session prep separates community plugins that need approval from enabled plugins (F-021). Pending selections stay checked for consent after creation, but no longer inflate enabled counts, runtime previews or world-data preflight. A notice explains that their world data is imported once after approval and enablement; the game can still start. See World data.
- Session world labels follow the edition being played. The header, chapter banner, opening summary, and window title use the session's content language even after switching the interface language (F-057).
- Plugin random ranges behave the same in tests and games.
makeRandom().int(min, max)and seeded or unseededctx.random.intshare argument checks andRangeErrormessages. Equal bounds, fractional bounds, and ranges whose width is at least2 ** 48are rejected everywhere (F-128). - Subscriptions no longer read the session for every frame (F-201). Producer incarnation tags and ordered deletion controls isolate replay and live events across same-ID recreation. Transport resets trigger a recheck and reconnect; stale frames are dropped while a still-current subscription stays open. Unauthorized connections cannot evict replay state, and hidden writes and empty batches avoid event-related session reads.
- Failed turns explain how to fix model errors. Runtime failure events retain the provider failure category, and the turn UI reuses the connection test remedies while keeping the provider message in details. Concealed runtimes keep their fixed error text.
- Account failures skip backups with the same credentials. After a refused key (401) or a balance or billing failure, a call skips targets with the same provider, base URL, and key, while still trying configured independent targets. A 403 or a rate limit still tries a same-account backup, because it can apply to one model only.
- Plugin author tests use current contracts. Tool integration tests compile a valid manual function manifest; runtime case files reject misspelled fields and malformed expectations before execution, with file and field paths in errors (F-128, F-130).
- New plugins start with matching versions. Every scaffold mode sets both package and manifest versions to
0.1.0, includes a runtime-case editor schema, and is covered by static validation (F-131). - Plugin text wires receive clean prompts and session-language instructions. Internal cache markers are removed at the wire boundary, and structured-output instructions and the Anthropic history opener follow the session instruction language (F-118, F-119).
- Explicit model targets are checked before provider requests. Speech, transcription and music reject incompatible slots or presets with
CONFIG_ERROR(F-168). - Media lists and job lists use neutral defaults. Gallery and preview downloads use the media format instead of a forced PNG name, and audio/video controls remain directly usable.
- GraphCanvas supports unweighted edges. Strength, polarity colors, and descriptive mappings are optional; unweighted edges use neutral styling and the graph empty state can be customized.
- Memory search keeps distinct hits and returns the relevant passage. Vector recall and keyword top-ups deduplicate by message ID; vector recall and archival use the same query-centered excerpts as keyword search. The tool preserves these excerpts and describes queries without assuming a keyword-only backend (F-154, F-155).
- Plugin labels distinguish default selection from source. Default plugins are labeled “Enabled by default” and remain switchable; bundled plugins are labeled “Builtin”. Session prep groups plugins by their first declared tag, including custom tags.
- Stage decisions show only the recap supplied by the slot provider. Missing or empty recaps no longer copy story text. Narration and free input remain available without suggestions; turn-scoped choices wait for a matching story turn, while choices that omit the optional turnId remain unscoped.
- Function text calls use their declared model role by default. Calls without
presetIdnow usefunction.model; an explicit handler choice still wins. Session model overrides remain agent-only, and other generation capabilities keep their own roles. - Function model roles are visible before and during play. The preparation screen and session plugin list show the declared role, resolved model, fallback label, and unavailable state as read-only text. See Model roles.
- A browser-private session no longer gets stuck after an interrupted turn and a world edit. When the page closed right after a turn's result was saved, and the world was edited before the next turn, every later action failed with "already committed with different data". The result and the end of the pending turn are now saved in one write, and a repeated result is recognised by what the server sent, not by the browser's copy of the world. Flow:
docs/internals/storage.md. - An agent guard reads its runtime's input bindings.
ctx.inputswas missing in a guard, so a guard could not see the upstream values its runtime declares inio.inputs. tabletop-rulesfollows the instruction-language rule. Its check instructions now followCOVEL_INSTRUCTION_LOCALElike every other bundled plugin, and attribute names resolve by language code (rufor aru-RUsession) through the SDK'sresolveI18nText.- Entry badges use their provider's localized labels. Codex, quest and world-rule panels declare their category labels in their specs; cards use provider-owned category and rarity options with neutral defaults for missing metadata.
- Historical plugin messages show their provider's title. Checks and quest changes no longer appear under “Earlier suggestions”. Candidate lists use neutral default labels, and unused branch-reply strings have been removed.
- Collection rollback respects the world deletion lifecycle. Worlds that acquired sessions during installation are retained, and failures report which packages were removed and which remain.
- Collection archives share one 200 MiB extraction budget. GitHub preview, batch installation, and offline ZIP import reject oversized sets before writing packages; repeated references to one repository commit count once.
- Plugin inputs now follow their declarations: dice receipts require
checks, plugin-data reads use direct values, and projected narrative bindings contain strings. Obsolete resizer/preloader parameters are removed, and missing plugin labels no longer display a runtime ID. Revisionless settings backends and normal missing-data degradation remain supported. - Multi-pod event fan-out no longer loses events silently or grows without bound. When a PostgreSQL NOTIFY stalls, each session queues at most 256 frames and drops the rest with a warning; the other pods see the gap and tell their clients to reload. A receiving pod keeps one ordering state per sender and session, reclaims it when the sender starts a new stream, and evicts the least recently used one, delivering any parked events and resetting clients first. Rules:
docs/reference/protocol.md. - Function runtime text, object, and evaluation calls use the remaining execution time instead of an extra 120-second gateway ceiling. HTTP retries, fallback targets, and a stalled resend share one transport allowance.
- The process-wide text concurrency limit now covers gateway calls from functions, background jobs, and direct callers as well as agents. Streams hold their slot until they finish or stop; queued agent calls retain their execution-time credit.
- GitHub previews show package descriptions in the UI language. Plugin locale files and world translation overlays remain available as
I18nTextthrough installation and update previews. - Plugin code notices include all declared imports and TypeScript module files. Preview and ZIP metadata use the loader’s import enumeration, including world projections, with
.mtsand.ctsfile detection. - Every plugin install path requires self-contained runtime files. GitHub, ZIP, offline collections, and updates reject nonempty runtime dependency declarations with instructions for the author; development dependencies remain allowed.
- Manifest errors name the file and the field you wrote. A cross-field error in an inline
runtimenow namesPLUGIN.md, and errors use the authored paths (schedule.detached,io.visibility,agent.completion.require) instead of internal names (turnCompletion,outputKind). - The
schedule.aftercontract description no longer says it must be a dependency. Anaftercontract only orders and does not have to be listed inrequiresoroptional. validatePluginManifestchecks aPLUGIN.md. It validated the frontmatter against the compiled runtime schema and rejected every real package manifest; it now uses the package manifest schema.- A runtime that can emit events stays in the foreground. A
schedule.detachedruntime that listsemit-eventin its tools, or declaresagent.advertiseEvents: true, is no longer moved to the background; the check read a package field that was always empty on a runtime. - A generation that ended for a reason the provider did not explain is no longer taken as complete.
finishReason: other(for example Anthropic'spause_turnor DeepSeek'sinsufficient_system_resource) used to be read as a normalstop. A story keeps the text already written and the app says it is not finished, so the player can retry the turn; other runtime answers, world generation and the history summary fail instead of saving half a result. The vocabulary also maps more documented provider words (Mistralmodel_length, GeminiSPII/IMAGE_SAFETY/MALFORMED_FUNCTION_CALL, CohereERROR_TOXIC/ERROR_LIMIT). Rules:docs/reference/slots.md. - Failed model calls count the tokens the provider reported. A refusal, an error finish reason, an error body or a stream that reaches EOF without its terminal event now carries the reported usage in
error.details.usage; it reachesllm.responded/gateway.failed, and the debug page's cost view countsgateway.failed. Model calls made by kernel extension providers, such as history compaction, are traced asgateway.*events under the provider, so their cost is no longer invisible. - Character Traits reads the session character schema through the generic
session.characterSchemapanel source and updates after schema commits. - Plugin state tables stay under
gameState.tablesand cannot overwrite World Model fields. Malformed dimensions show a local notice while the opening scene remains usable. - Resuming a community plugin requests missing server-code and runtime approvals before execution, rechecks grants under the session lock, and retries after the player approves. Denial or switching sessions retains the pending suspension (F-172).
- Invalid resume schemas now refuse execution with
resume_schema_invalidinstead of bypassing validation. The suspension stays pending and can be abandoned (F-174). - Image navigation opens the panel that declares its destination. Right-panel specs can declare
navTarget: "images"; an image icon alone no longer captures navigation. Once panels finish loading, missing destinations clear the request and show a notice. Lazy loading and explicit loading retries retain the request. - The SDK README and help texts match the package. The README states the real
zodpeer range and every subpath export, thetool()example imports from@covel/plugin-handlers-utils, thecovel-test-runtimehelp uses a scaffolded plugin, and theplugin-with-toolstemplate no longer ships an.npmrcthat pointed at a file it must not have.docs/plugins/essentials.mdlistscovel.registerWires→contributes.wires. - A world with dimensions can be played without a dimension plugin. When no active plugin provides
world.dimensions@1, creating a session failed with a server error ("Dimension provider unavailable"), and so did the world-data preflight and sync. The dimensions are now left out: the session starts without them, the response lists them inignoredDimensions, the preflight and the session-prep screen warn (dimension-provider-missing), and a sync keeps the dimension records the session already has. - Refreshing during a turn shows the plugin panel and commands at once.
GET /api/sessions/:id/pluginsreads the saved session record without waiting for the running turn or for background jobs. - A background job no model can run no longer slows every action. When a
schedule.detached: { settle: true }job has no usable model credentials, the next action stops waiting up to two minutes for it; the trace recordsexecution.settle-skippedwith the jobs it skipped. - Session locks are safer. Background work started while a session lock is held no longer counts as its owner, and taking several lock keys at once sorts them inside the lock, so two overlapping requests cannot deadlock.
- Settings controls expose their names, state and results to assistive technology. Operation notices announce success or failure, configuration sources use segmented buttons, provider and proxy inputs have labels, capability and dark-mode toggles expose their state, and the settings search retains a visible focus ring.
- Localize settings text, units, plural counts, numbers, and dates; use theme warning colours and report import and desktop operation results consistently. Hide custom-only settings from the generic form.
- Resolve theme fallbacks without rewriting saved preferences, show advisory contrast checks on theme import/save, translate builtin theme labels in every UI catalog, and constrain world accent lightness.
- Load worlds with invalid accent colours using the default accent and a server warning; keep format and lightness errors in
pnpm validate:worldfor authors to fix. - Accept nested CSS selector lists in theme imports, reject builtin theme IDs with localized errors, and confirm before save-as replaces an existing custom theme. Preserve themes changed during confirmation.
- Unsaved prose remains readable after failed stream recovery. Once recovery confirms a failed or interrupted turn, the web client labels the local text as unsaved and offers the available retry beside it, without mixing it into saved history or stage content.
- A world without a descriptor is read by the plugins its session runs. Before, any installed plugin, even one that failed to load or was never enabled, could make a conventional file such as
data/quests.yamlstop being imported for every world. Now only the session's active plugins count. When two of them name the same file, the session still starts without it, the server logs a[sessions]warning and the world data preflight returns aconvention-collisionwarning. pnpm validate:worldchecks a world against the plugins a new session starts with. A source whose receiving plugin is not enabled by default gets anunrequested-receiverwarning, and a file that several scanned plugins name getsconvention-collision(an error when a default session enables two of them). Records are still checked against every scanned plugin.- Edits survive a removed world package. Startup cleanup keeps edited world records even when the original package disappears and no session uses them.
- Edits to a bundled, ZIP-installed, hand-placed or generated world survive a restart and a file change. Only GitHub installs were marked
packageModifiedwhen edited, so the startup seed and the file watcher silently replaced every other edited world with its files. Every world that came from a package is now marked, and the seed, file reloads, translations and updates keep the edited record. - New players no longer inherit a lore character's name and biography from generated schema defaults. World initialization clears generated biography defaults, and player creation uses the submitted name for declared text name attributes. Both schema-generation prompt languages explain that defaults belong to a new, unnamed character. World Dimensions is now 0.0.42 and Character Creator is 0.0.41; see
docs/reference/tools.md. - The session header's "More" menu opens over the notice bars below the header. Before, a menu item that lay over a notice bar (for example "Your selected community plugins need authorization…" or "World dimensions are paused…") could not be clicked.
- Long sessions no longer get slower on every turn because of how history is read. SQLite and PostgreSQL now have indexes for the turn-message reads made on every turn: the prompt history, recent messages and the player-message count. They also have indexes for snapshots, and PostgreSQL gains the turn-result index SQLite already had. Pruning old auto snapshots is a single statement that no longer scans every session's snapshots. Existing databases get the new indexes at their next start.
- A function runtime that a
PostRuntimehook turns from failed into success passesoutput.schema. The recoveredoutputis checked like a normal success, and the writes the handler buffered before it failed are not committed. - Interrupted installs and removals recover at startup. Unfinished staging files are cleaned up, pending world deletions can be retried, and plugin uninstall moves the whole package out of discovery before deleting files (F-177, F-182).
- World packages cannot supply their own installation receipt. ZIP, GitHub, and collection imports reject
.covel-install.jsonand case variants before installation, preventing forged source and update metadata (F-178). - Forking past a deleted picture works. A fork whose history names media that is no longer stored (for example, removed with "Delete" in Settings → Stored media) used to fail with
409 media_reference_missing. It now goes ahead: the child keeps its messages and data, shows the "media unavailable" placeholder there, and thePOST /api/sessions/:id/forkresponse reportsmissingMediaCount, the number of distinct missing files. Media that exists but that the parent may not read is still refused with403 media_reference_forbidden. - World-data sync no longer takes away pictures a session has already shown. When a sync drops a media index row, the session keeps its reference if the file still appears in its other index rows (identical files share one ID), messages, snapshots or other history. Pictures in earlier turns stay readable, and forks of snapshots from before the sync keep working.
- Media generation no longer slows down as other sessions add media. The cache lookup behind
ctx.images.generate(),ctx.speech.generate()andctx.music.generate()now reads only the calling session's own assets (SQLite and PostgreSQL query by owner, using the owner index) instead of the whole media inventory. MEDIA_BACKEND=pgwithoutDATABASE_URLstops the server. It used to turn media off without a word, so every media route answered 503. SetMEDIA_BACKEND=noneto run without media on purpose.- Temporary media references that a crashed world-data import left behind now expire.
POST /api/media/cleanupreleases every temporarymedia-pin:<uuid>reference created 24 hours ago or earlier: a dry run lists them in the newexpiredRefsfield of the result, and a real run removes them, so their bytes can be reclaimed like any other unclaimed media. A younger reference keeps protecting its file, the limit is fixed, and nothing expires outside that opt-in endpoint. No stored schema changed.MediaLifecyclePolicygainsexpireRefsandMediaCleanupResultgainsexpiredRefs. Rule:docs/reference/media-store.md. - Fresh media can no longer be deleted by a cleanup that runs while it is being saved. Uploads,
ctx.media.put()andctx.media.ingestUrl()now store their bytes together with a temporarymedia-pin:<uuid>reference and drop it once the session owns them. Before, "Delete all unused" orPOST /api/media/cleanupcould remove an image between the two steps and leave a placeholder. Rule:docs/reference/media-store.md. - A failed multi-image
ctx.images.generate()call leaves no images behind. Every image is stored first and the session claims them together in one transaction (MediaStore.claimAssets(), also used by uploads and world-data imports). When one image fails to download or save, or recording a claim fails part-way, none is claimed and the bytes are left to media cleanup, instead of a partial batch that stayed in the session. - A failed session delete keeps the session's media readable. The session is deleted before its media claims are released, so a delete that fails no longer leaves a session that shows placeholders for its own pictures. When finishing a new session fails and deleting it fails too, the import's temporary references are still released.
- Semantic recall no longer loses a message saved late. The recall index moved along message timestamps, so a background execution's message, saved after a later turn had been indexed, was never embedded. It now moves along the order messages were saved; a plugin's
readTurnMessageshad the same gap and is fixed with it (F-025). - Archival memory no longer rereads the whole corpus every turn. After each turn the archival index read every character, lorebook entry and searchable plugin data record and rewrote a hash of all of them; a semantic archival search read them all again. It now reads only the records changed since its last position (deletions are kept as tombstones), skips embedding when the stored vector already has the same text, and a search reads only the records its hits name (F-039).
- Narrative Only and the minimal-set reset no longer retain World Dimensions by default. The pack excludes
world-init, and the reset keeps only narrative plugins and the dependencies declared by them and the world. All bundled worlds can start with only the narrator active; dimensions are skipped with a notice when their provider is inactive. - On PostgreSQL, a busy lock pool no longer shows an idle session as running. When the server cannot get a lock connection within 200 ms,
GET /api/sessions/:id/execution(and theexecutionfield of/view) now answersunknowninstead ofrunning; a lock held by another process still answersrunning. The client shows that it is checking, checks again less often each time, and after fiveunknownanswers stops and asks the player to choose "Check status". Anunknownstatus never offers a retry, moves unsaved text away or finishes a turn's steps. Rules:docs/reference/api.md. - Concurrent session metadata updates of different keys no longer lose one another on PostgreSQL.
updateSessionmergesmetadatainside one UPDATE there, and a key set toundefinedis removed on every backend. Two updates of the same key still have one winner. Rules:docs/internals/storage.md. - Plugin data writes use the same rules for every plugin. The management API now allows
PUTandDELETEfor activekind: coreplugins, just as for other plugins. Reserved namespaces and inactive plugins remain protected. Seedocs/reference/api.md. GET /api/framework/capabilitieslists the builtin tools the server runs. It used to return a hand-written list that had drifted from the registry.pnpm validate:worldalso warns about an ID that is a UUID. A field namedid, or ending inId/Ids, that holds a UUID is left out of what a model reads, like a write time. Themodel-hidden-fieldwarning now reports it too and suggests a readable ID.- A development hot reload of a community plugin now picks up changes in the modules its entry, handlers and guards import. Editing a file such as
lib/helper.jsused to reload the plugin with the old helper until the server restarted. A reload now gives the plugin a fresh module graph: relative ES module imports that resolve inside the plugin directory are evaluated again, while files outside it,node_modulespackages,.cjsand.nodefiles stay shared. Production, bundled plugins and the first activation resolve modules as before. Each reload leaves one more copy of the plugin's modules in memory until the server restarts. Rules:docs/reference/plugin-extensions.md. Underpnpm deva change to a code file the server has already loaded still restarts the whole backend, becausetsx watchfollows every loaded module; the reload described here is what runs when the server is started without that watcher, and for manifests, UI files and translations. - The with-tools plugin scaffold uses the public SDK to build proposal envelopes. Static checks now reject direct random sources in plugins and templates, and keep both known-limit ledgers in sync with source markers.
- A malformed write from an optional plugin no longer fails the whole turn. A proposal such as a
narrative.appendwith nocontent, or aplugin.datavalue with a BigInt or a circular reference, used to make the store write throw on SQLite and PostgreSQL. That rolled back the turn and its story, so every turn of the session failed. The memory backend stored an empty message instead. Every proposal is now checked against the schema of its type before it commits. A malformed one is rejected with the codeproposal-invalid: the plugin's writes for that turn are dropped and the story is kept. Values are committed as their JSON copy, so all backends store the same thing. - A write that refers to a picture the session does not hold is refused at commit. A
MediaReffrom another session, or one whose file was deleted, was saved into messages and plugin data, and failed only later, when the session was forked (403/409). EveryMediaRefin a proposal is now checked against the session when the proposal commits; one that fails rejects the proposal (media-not-found,media-ownership-invalid,media-mime-mismatch). A ref that passes is stored with the stored MIME type and size and withouturl. - The plugin list keeps each plugin's tags in their order.
GET /api/pluginssorted them, so a derived tag came before the ones the author wrote. The prep screen now groups plugins on purpose by whether they call a model ("Calls a model", "No model calls"), and a plugin that has no such tag under its first tag. - A plugin pack chosen after "Back to the minimal set" gets its plugins back. The minimal set excluded the pack's plugins for good, so choosing a pack afterwards left them off.
- World lists name each world in the edition the player gets. The showcase, the other list layouts and the delete and language dialogs showed a world's own language (an English interface showed the Chinese names of worlds that have an English edition).
- The world detail page shows dimension values in the play language. Values kept as locale maps printed every language, booleans and empty values printed
falseandnull, and dimension names ignored the play language. Its update note no longer tells the player to open the page they are on. - Skipped tasks say why in words. The turn's task list printed codes such as
upstream-failed; the needs gate now reports that code like the input gate, and the app translates every gate code, also for a turn shown again after a reload. - Approval dialogs name the plugin. Turning on a community plugin asked "Plugin qa-broken requests to run covel:plugin-server-code"; every approval now names the plugin by its display name, and the server-code approval explains what it allows.
- The World item of the top bar leaves world details and the dimension editor. It did nothing there; the editor asks first when it has unsaved changes.
- A world whose package changed on disk shows its update without a reload. The world list is fetched again when the player comes back to it and when the window regains the focus.
- An interjection stays where the turn saves it. It showed below the streaming story text and, after a reload, above it; it now goes above it at once, and the text written after it starts a new paragraph while it streams.
- Lists in player text use the interface language's separators, and the session hints no longer show contract IDs such as
world.dimensions@1. The hint for installing a plugin ZIP says wherePLUGIN.mdgoes. A plugin's name on the prep screen is no longer cut short by its badges, dimension tables in the narrow side panel scroll instead of squeezing a column to one character per line, and the Russian language notice reads correctly. - "Revise this world" no longer destroys files a person added to an AI-generated world.
.covel-generated.jsonnow lists every file the app wrote into the package with a hash of its content. When the package holds a file that is not in the list, or a listed file whose content was edited,POST /api/ai/revise-worldreturns409 world_package_modifiedwith the files indetails.added/details.changed, the world page names them, and the package is left as it is. The check runs before the model is called and again just before the package is replaced. Translations made in the app are recorded in the list, so a translated world can still be revised. Rules:docs/reference/api.md(revise-world) anddocs/reference/world-data.md. - Generated worlds can finish setup after character schema generation. The world initialization output contract no longer requires the completion signal that the kernel separates from business output. The contract still validates both generated and author-provided schemas; see
docs/reference/plugins.md. - A model role that is not set up no longer shows as an error. The prep screen and the session's plugin list said "Model role plugin is unavailable" for a role the player never defined, although the game runs it on the first text role. They now say "plugin is not set up; uses story · …"; a role that is defined but cannot be used is still reported.
- Reply Variants is listed as calling a model. It asks the fast role for alternative phrasings, and now declares that role, so its tag reads "Calls a model".
- SQLite saves can be deleted when the optional vector extension cannot load, even in databases with existing vector indexes. Orphan vectors are reclaimed when the extension next loads successfully (F-017).
- Concurrent startups cannot both take over a stale SQLite database lock. Takeover is serialized, uses a rename and an ownership read-back check, and counts relative and absolute paths as the same lock (F-113).
- Stopped openings offer Continue, and messages stopped before a turn starts return to the input box. Newer draft text is preserved in both text and stage views.
- The world editor confirms before navigating away with unsaved changes. In-app links and Back now use the same discard prompt as Cancel, including incomplete form input and unapplied JSON.
- MemoryStore, SQLite and PostgreSQL now agree on four storage rules.
- Rows with the same timestamp come back in the same order on every backend: events, state changes, interaction records, snapshots and suspensions break ties by ID, and state schemas are listed by table name, so an update no longer moves a row on PostgreSQL and snapshots and checkpoints list them in a stable order.
- MemoryStore rejects a second turn result, tool call, runtime output, interaction record, state change, event, trace, player input or session summary under an existing ID, as SQLite and PostgreSQL always did.
- A batch of lorebook entries is written whole or not at all on SQLite and PostgreSQL.
- A transaction no longer copies every MemoryStore collection when it calls
queryTurnResults,queryTraceEventsorqueryPluginData.
- Schema startup errors identify the database that was refused.
StoreSchemaVersionError.databaseand its message include the absolute SQLite file path, or the PostgreSQL host, port and database name without credentials. SQLite errors explain how to move aside or delete disposable development data. The development-path documentation now distinguishes an existing Covel home from theapps/server/data/covel.dbfallback. - Abandoning or expiring the last suspension of a turn ends that turn. The turn now counts and the detached background jobs it held back are queued, as when the last suspension resumes; before, they were dropped. The expiry sweep settles each record under its session lock.
- Progress jobs of a suspended runtime no longer wait forever. Jobs a function runtime reported before suspending end
succeededwhen the resume commits andcancelledwhen the suspension is abandoned, expires or is voided. - An interjection is saved with its turn. A message sent with "interject" during a turn used to be saved to the chat log at once, but never to the conversation history that later turns read, so the model forgot it after the turn; and a stopped or failed turn still left it in the chat log. It is now saved when its turn is saved, to both, in the same transaction, after the player's message of the turn. A turn that is not saved keeps neither. It does not count as a new player turn.
pnpm validate:pluginchecks a plugin's UI spec files. A misspelt component name, a spec with neither or both ofviewandwebview, arepeat.statePaththat is not an absolute path string or{ "$item": "field" }, and an$item/$bindItemfield name with a leading slash now fail validation with the spec file and the JSON path of the node, instead of showing only as a missing panel in the app. The ZIP install runs the same checks, so a package with such a spec is refused. Rules:docs/reference/plugins.mdanddocs/reference/ui-panels.md.- Opening a session no longer misses a background change committed at the same moment. The session view now carries the event cursor of the moment it was read. The first live connection replays from it, or reads the session again when the connection opened later than the view, so a panel no longer shows an old value until the next change or reconnect.
- Worlds start even when their data receivers are not installed. Session creation, preflight and later imports or sync skip the unavailable contract writes with warnings through the existing world-data notices. Active projections still receive the source; sources with no consumers skip schema checks and media import too. Sync preserves existing managed data, ledger rows and media references for skipped writes, while successfully read empty data still removes old records when receivers return. Author validation keeps its existing error rules. See World data.
- World update warnings no longer direct players to an unavailable sync control. Chinese, English and Russian warnings explain that updates preserve data already imported into an existing game and that new starting data requires a new game.
- Opening character defaults are checked in the sentence the player will send. Character Creator returns a filled preview for model review, rejects adjacent repeated words and explicit undecided placeholders in English and Chinese, and asks for a concrete calling from the world. A revised form replaces its earlier draft before display. Preview and submission share the same template renderer.
- Dice checks no longer offer health or trust as a check attribute. Dice Check listed every ranged number of the player character with its modifier, so the narrator could roll against health (80/100 → +8) or trust. It now lists only the
abilitiesattributes, the same ones Tabletop Rules uses for point buy and its check panel. A world without ranged abilities gets no list, and the narrator converts the attribute itself as before. - Emberback's five attributes are all abilities. Grit and Insight were
stats, so the status bar showed them as bars, and Rapport wassocial. All five are check attributes from 0 to 5 and now sit inabilities. Sessions started earlier keep their old character schema. - A plugin's world data file may end in a suffix such as
.bakor.min.authoring.source.pathrejectedcharacters/characters.bak.jsonormedia/scenes.min.jsonas a language version of a kernel file. Now only a two-letter language code (.fr,.zh-CN) or a three-letter one with a region (.yue-HK) makes a language version. Seedocs/reference/world-data.md. pnpm i18n statusandpnpm i18n lockgo on past a plugin they cannot read. A plugin whose runtime directory holds a misnamed manifest stopped the whole run; now the command names that plugin and its problem, checks the others and exits with status 1. The translation check inpnpm checkreports such a bundled plugin the same way.- Two Emberback world rules fire in Chinese sessions.
emberback-future-echoesandemberback-signal-evidencenow have Chinese trigger words; before, they only matched English words in the player's message. - Emberback no longer contradicts itself about Mile Seventeen. The repeater sits beneath the arch everywhere, and every line repeats the warning as "do not let the convoy reach Mile Seventeen".
- World translation includes lowercase trigger words.
i18n statusandi18n translaterecognize everykeyslist, and translating other text preserves existing translated lists. World validation warns when a translated rule or lorebook entry still inherits its original trigger words. See world translation tooling. - Lantern Barrow's and Emberback's rules text gives the attribute modifier the dice actually use. Both worlds said checks were "d20 + attribute", which reads as +4 for an attribute of 4. Both dice plugins turn an attribute into a modifier over its declared range, so the two worlds' 0–5 attributes give +2 per point (+10 at 5). The Chinese and English world text now say so; the difficulties and the natural 20 / natural 1 rule are unchanged.
- Provider failures keep their HTTP status when the response is empty or not JSON. Authentication errors are recorded as
auth, error messages keep a bounded body preview, and calls that cannot be retried report a failed call instead of exhausted retries. See model failure reasons. - Chinese tabletop turns without a submitted check keep the narrator's no-check marker. Both narrators can recognize that ordinary action checks still apply.
- Scene backgrounds follow the named place before another scene's sub-location. In Harukaze Academy, "Library window seat" now selects the library instead of Class 2-B in both Chinese and English. Exact sub-location matches still work, and the longest name wins within the same rank. See scene matching.
- Model capability controls use the same input, output, and feature names as the server. Custom model output choices now include
video, as documented in the slot reference. - World rules fire on the words English players type. The trigger words of the three worlds' rules and lorebook entries now cover the common English forms of each idea instead of literal phrases that almost never matched (for example "three bells", "appraised", "favoritism", "singing").
Security
- Sensitive desktop IPC rejects child frames. Reading keys or the REST token and changing settings requires a trusted main frame.
- Listing saved key providers requires the configured desktop token.
GET /api/config/keysnow uses the same guard as sensitive configuration routes (F-107). - Public hosted model catalogs hide gateway addresses.
/api/presetsand/api/llm-configomitbaseUrlunless the caller has operator access (F-108). - Plugin localization tools reject executable frontmatter. Loading, installation, validation, translation, and authoring checks share the same plain YAML manifest reader. Runtime directory symlinks are excluded consistently from discovery and localization scans.
permissions.httpcovers a community package's entry code.covel.httpin tools, hooks, RPC, form validators, services, extensions and wires reaches only the origins and methods in thepermissions.httpof the rootPLUGIN.mdand of its runtimes; a wire may also reach the endpoint of the slot it serves. A service or extension handler'scontext.utilsis checked against the provider's package, not the caller's. Builtin packages are not checked, as for runtimes. Seedocs/internals/security.md.- Media wires serve only sessions that enable their plugin. Image, speech, transcription and music wires a plugin registers follow the same rule as text wires; generation outside those sessions fails with a configuration error that names the plugin to enable.
frameworkis a reserved plugin ID. A package withid: frameworkno longer loads or installs, andpluginId: "framework"in plugin-rpc reaches only framework default actions. A plugin ID no longer falls back to a framework default action of the same name.- Covel's own requests no longer follow redirects. The model database download and the desktop version check followed a redirect to wherever it pointed, past the address checks, so an answer that redirected to an internal address (such as a cloud metadata service) was fetched. Every framework request now fails on a redirect unless its code checks each hop itself, and the version check reads the release answer with the same size ceiling as other provider answers. Rule:
docs/internals/security.md#redirects. - The local server answers only to loopback host names. On the
selftier (the default, the desktop app andpnpm dev) a request whoseHostheader is notlocalhost,127.0.0.1,[::1]or a name in the newCOVEL_ALLOWED_HOSTSvariable gets403 host_not_allowed, on every method. A web page can no longer point its own domain at your machine (DNS rebinding) to drive the token-less API.demoandcommercialare unchanged. Rule:docs/internals/security.md. - Forking a session on a hosted tier needs the operator token (F-204).
POST /api/sessions/:id/forkcreates a session, so it now asks whatPOST /api/sessionsasks: ondemo/commerciala session owner can no longer mint new sessions by forking their own. Onselfand the production MemoryStore, an owner still forks freely. - A broken
llm.tomlno longer prints environment variable values. When a line with${VAR}had a syntax error (for exampleapiKey = ${DEEPSEEK_API_KEY}without quotes), the error quoted the text with the key already filled in, and it reachedserver.log, the Settings page and thePUT /api/config/raw/llm.tomlresponse. The error now gives only the line, the column and the reason. - Plugin load errors no longer reveal the server's install path.
GET /api/pluginswrites the plugin directory as<plugin>in theerrorof a package that failed to load. The full message stays in the server log. - Provider keys only reach endpoints the player or operator configured. No plugin manifest or configuration file can point model calls, with the player's keys, at an address of its choice. See
docs/internals/security.md#provider-keys.
Documentation
- Desktop key storage documentation now describes the IPC path. The UI receives plaintext keys in memory; the provider-list markers belong to the optional REST transport.
- Documentation diagrams use Mermaid 12. The documentation site now requires an ES2024 browser (Safari 17.4+) and keeps the existing flat shapes, layout and label widths. This browser requirement applies only to the documentation site.
- Multiple diagrams on a documentation page render reliably. Each diagram now has its own SVG ID, so diagrams drawn together or after a theme change cannot overwrite one another.
- Search in the Chinese documentation finds a word in the middle of a phrase. The search box split Chinese text only at punctuation, so a query matched the start of a clause or nothing: the Chinese for "preset quests" did not find the page about them. Chinese text is now indexed from every character on, in pieces of up to four characters. The Chinese search index is larger for it (about 2.1 MB compressed, from 0.9 MB); it is loaded when the search box is first opened.
- Covel has a documentation site. The "Docs" link of the showcase site opens it at
docs/instead of the repository'sdocs/directory on GitHub. It has a sidebar for each part (guides, reference, internals), full-text search, a table of contents on each page, rendered diagrams, a dark theme and a language switch. Its pages are the Markdown files ofdocs/, built by the newapps/docspackage (pnpm --filter @covel/docs dev). - The documentation is rewritten for a reader who meets the product for the first time, in plain Chinese. The chapters follow that reader: getting started (install, connect a model, first session, packages, backup and upgrade, run from source), what ships with Covel, creating a world, developing a plugin, verifying and shipping, common tasks, reference, and internals. The first-world and first-plugin guides build a real package step by step, and every command they show was run.
- The documentation has an English edition. Every page has an English page at the same path under
docs/en/, served atdocs/en/. The showcase site's "Docs" link opens the edition of the visitor's language. The Chinese pages are the source. - A chapter on what ships with Covel.
docs/builtin/describes the gameplay packs, each bundled plugin (what it does in play, what the player sees, its settings, what a world can preset for it) and the app's own features. - The changelog is a page of the site (
docs/releases.md, which includesdocs/CHANGELOG.md). - The pages moved, and a link to an old path needs the new one.
docs/guide/anddocs/architecture/are gone anddocs/reference/holds the rewritten reference:docs/architecture/<name>.mdisdocs/internals/<name>.md; the guides are underdocs/start/,docs/worlds/,docs/plugins/,docs/ship/,docs/recipes/anddocs/internals/;docs/guide/env-registry.mdanddocs/guide/desktop-config.mdare indocs/reference/;docs/glossary.mdisdocs/reference/glossary.md; the English pages that were<name>.en.mdaredocs/en/<same path>.md.docs/README.mdlists every page. Heading anchors of the reference and internals pages are unchanged. The links in the released sections ofdocs/CHANGELOG.mdwere repointed the same way. - Reference pages were corrected against the code while they were rewritten. Among others: the HTTP API page shows
nextCursoras the opaque string it is and no longer lists error codes that do not exist; the SSE page lists seven action types and the realSseEnvelope; the tools page lists the dimension proposals; the technical-debt page matches theponytail:markers in source. - Bundled descriptions and create-plugin guidance match the current API. Dice use
ctx.random, function documentation has no Chinese prompt variant, tabletop creation reads the World Model, and character handles make no assumption about ID shape. The skill points to public result/store types and uses complete runtime IDs and author SDK imports. - Stage choice documentation reflects the current slot contract. Removed the obsolete icon/color unpacking debt and old guide-data explanations.
- The hook reference says which events
ctx.signaldoes not cancel, and thatPreToolUsealso fires forctx.tools.call.docs/internals/flow.mdlists every write outside the commit transaction. - The component catalog states its limits. General tables, statistical charts, custom timers and animation, and unlisted icons use webviews; existing specialized tables, transitions and media remain available.
- Fields no bundled plugin uses moved to an advanced part.
covel,homepage,author.about,author.links,contributes.wires,events[].advertise,prompt[].position.depth,commands[].context,arguments[].variadicandchoices, the settings controlstext,textarea,integer,sliderandslot, and the agent loop fieldsmaxSteps,firstTokenTimeoutMs,idleTimeoutMs,loopDetectionandmaxRecursionDepthare listed in "Advanced fields" ofdocs/en/reference/plugins.mdand "Rarely needed fields" ofdocs/en/plugins/advanced.md. They all still work. - The product principles are written down.
docs/internals/design-principles.mdnow opens with them: players come first; bundled plugins are demos on equal footing with community plugins, and the framework runs with every plugin removed; a missing plugin or an unused field must not stop the game; one core serves the desktop app, the self-hosted web app and, later, the official web app: the desktop and self-hosted apps are the current focus, the official web app is not supported yet, and the core stays generic so that it can be added without rewriting it; manifests ask only for what cannot be derived or defaulted; and contracts change directly during fast development.AGENTS.mdsummarises them for coding agents. - Pages that contradicted the principles or the code are corrected. "Core plugin" no longer names every bundled plugin: a
kind: coreplugin joins every game by default, and the player can turn it off on the session-prep screen (the tables ofdocs/builtin/index.mdsay "default-enabled" instead of "always enabled"). The storage and HTTP API pages describe the three deployment forms instead of three tiers and say that the official web app is not supported yet: SQLite is the default, PostgreSQL is for professional developers and special needs, and the two must behave the same. Thecommercialtier is reserved for a possible commercial edition that this repository does not develop.docs/reference/slots.mdno longer says that a manifestwiresfield is still accepted. The pages on the bundled worlds say which play mode each one demonstrates, and thatemberbackis written in English to show a world in several languages. The glossary has "Bundled plugin", "Deployment form" and "Official web app", and the empty pagedocs/en/reference/orld-installation.mdis gone. - UI references use the current component names and document all four stage slot values. Chinese and English pages now name
MediaGalleryandJobList, require explicitalwaysRender, and describe stage fields, limits, speaker-name strings and provider validation. - World data sync is documented as an API for authors and tools. The Chinese and English references explain explicit sync, conflict handling and its limits for existing sessions. The player guide directs players to start a new game for new starting data.
- The world manifest reference, "Your first world" and the world generator's tool now say what each attribute
categoryis for:abilitiesfor checks and point buy,statsfor status-bar bars,socialfor relationships.
Upgrade notes for v0.0.52
- A community package whose entry code calls
covel.http.fetchWithRetrymust list the origins it calls underpermissions.httpin its rootPLUGIN.md(or in a runtime); undeclared requests now fail withhttp permission denied. - Recreate pending development jobs that froze a turn digest before this fix. Existing snapshots are not rewritten and may still contain concealed tool arguments.
- A database written by the current build opens as before and is stamped with the version on its first start. If startup reports "Data from another Covel version", back up the data directory and set a new
data_root(desktop),SQLITE_PATHorDATABASE_URL. - If you run the PostgreSQL Docker stack, replace
pnpm docker:build/up/down/down-all/logswith thedocker:pg:commands. The old names now start a separate SQLite stack on the same default port. - Clear old development browser display caches after upgrading. The state-patch cache now stores bounded field values and recent event IDs. Save checkpoints are unchanged.
- Recover media omitted by an earlier copy manually. If the old data root still exists, copy its
media/directory into your backup before upgrading or recreating development data; copying the files alone does not convert an absolute-path database. If the old files were deleted and no backup exists, they cannot be recovered. - A self-hosted instance opened from other machines must list its names. If players reach your instance by a domain, a LAN address or through a reverse proxy, set
COVEL_ALLOWED_HOSTS(for examplecovel.example.com,192.168.1.20) before upgrading. Opening it atlocalhostor127.0.0.1needs nothing. Details:docs/reference/env-registry.md. - An existing
pgdata18volume keeps thevectorextension at 0.8.6 until it is updated in the database. After the new image starts, rundocker compose -f docker/docker-compose.yml --env-file .env exec postgres psql -U covel -d covel -c 'ALTER EXTENSION vector UPDATE;'(use yourPOSTGRES_USERandPOSTGRES_DBif you changed them). - A world generated by an earlier build can no longer be revised in the app. Its
.covel-generated.jsonhas no file list, so nothing says which files the generator wrote; after a restart the world loads withoutmetadata.generatedand "Revise this world" is not offered (409 world_not_revisablefrom the endpoint). Generate the world again to get a revisable one, or keep the package and edit its files. There is no migration. - Development worlds without a package digest need one restore to enable update detection. Back up app edits before restoring; the restore replaces world data and settings.
[0.0.51] - 2026-10-12
This release makes the download smaller and moves the showcase page out of the app. The app now opens on the world list; the showcase is a separate static website. Bundled portraits and scenes ship as WebP, which takes about 95 MB off the files that go into the installers and the Docker image. Without an llm.toml, the built-in model can be pointed at any OpenAI-compatible provider with DEFAULT_* variables. Three faults a static analysis found are fixed, and release installers carry a build provenance attestation. There is no breaking change: a database created by v0.0.50 works as it is. Read the upgrade notes before installing. macOS Apple Silicon and Windows x64 binaries are unsigned; the macOS build is not notarized.
Verified for this release: a session was started and played by hand in the development build. Not verified: the size saving is computed from the packaged files, not measured on a built installer; the art generation scripts were not run against an image provider after the change to WebP; the attestation step runs for the first time in this release's workflow; nobody played a session in the Windows build (it is built and packaged by the release workflow).
Added
- Point the built-in model at any provider with
DEFAULT_*variables. Without anllm.toml, the server and the runtime test harness used a DeepSeekstoryslot that nothing could change. SettingDEFAULT_BASE_URL(an OpenAI-compatible endpoint),DEFAULT_MODELandDEFAULT_API_KEYnow sends that slot to your own provider; left unset, it stays DeepSeek. The recorded sessions undertests/llm-replay/use the same variables:pnpm e2e:replay --recordrecords fromDEFAULT_BASE_URLwithDEFAULT_API_KEY, and--upstreamis optional. - A separate showcase website.
apps/siteis a static site with new sections and copy in English and Chinese, a light and a dark theme, and the four bundled worlds. Its download button gives a macOS or Windows visitor the installer from the latest release, with the Windows portable build beside it. Run it withpnpm --filter @covel/site dev;pnpm --filter @covel/site buildwritesapps/site/dist/. The demo media script moved with it:pnpm --filter @covel/site build:media. - The showcase website is published on GitHub Pages. A push to
mainthat changesapps/sitebuilds it and deploys it tohttps://ackness.github.io/covel/(.github/workflows/site.yml); pull requests only build it. The build follows the Pages base path, so a custom domain works without changes.
Changed
- The showcase website tells the plugin story. Its headline reads "Build your own …" and swaps the last words between visual novel, tabletop campaign, epic story and multilingual world. Each swap animates the words letter by letter and carries the subtitle, the colour, the screenshot and a list of the bundled plugins with it, lighting up the ones that mode really runs (taken from the bundled packs). Below, two cards explain the plugin design and world packs. A new section lists the built-in plugins, each with its plugin ID.
- The app opens on the world list.
/in the web app and the desktop shell now goes straight to the world list, and the Covel logo leads there too. The showcase page that used to sit at/is gone from the app, along with its video and images, so the desktop package no longer ships them. - The installers and the Docker image are about 95 MB smaller. The portraits and scenes of the bundled worlds ship as WebP (lossy colour at quality 90, lossless alpha) instead of PNG, 77 MB down to 10 MB, and the PNG originals of gallery pictures stay in the repository without being packaged; the gallery already showed their WebP versions.
scripts/generate-portraits.mjsandscripts/generate-scenes.mjsnow write WebP and needcwebp(libwebp) on PATH;scripts/encode-world-webp.mjsconverts a picture made elsewhere. Rules:docs/v2/worlds/art-direction.md.
Removed
VITE_ROUTER_DEVTOOLS. The TanStack Router devtools showed only on the old showcase page, so the button and its variable are removed.
Fixed
pnpm e2e:replayno longer hangs after a session. When the test server was stopped by a signal, the command waited forever for it to exit.- The showcase website shows the Covel icon. The header, footer and browser tab pointed at an
icon.pngthe site never shipped;apps/site/public/now has it. - The desktop startup screen is readable in light mode when startup fails. With the system in light mode, the error message and hint were drawn in near-white text on the light background, and the error title and the spinner's centre dot were faint. They now use dark colours in light mode.
Security
- A base URL with a long run of slashes no longer holds the server. The trailing-slash trim of a provider base URL took time quadratic in the length of a run of slashes that did not end the URL, and a base URL can arrive in a request's model settings (16,000 slashes cost about half a second per request). Every such trim in the model gateway, the evaluation adapter and the embedding lock is now linear.
- A prompt body cannot stall prompt assembly with
{{and whitespace.{{followed by a few thousand spaces or tabs and no}}took the template interpolation tens of seconds, in the kernel (interpolateTemplate) and in the author SDK (interpolate). Both now scan the template in linear time and fill exactly the same{{ path }}variables as before. pnpm i18ncannot be made to write intoObject.prototype. A manifest key named__proto__in the plugin being translated led the label writer (writePluginTranslations) out of the locale file's section; a label whose place holds__proto__,constructororprototypeis now skipped.- Release installers carry a build provenance attestation.
gh attestation verify <file> --repo ackness/covelconfirms that a downloaded.dmg,.zipor.exewas built by this repository's release workflow, and from which commit. The installers stay unsigned;SHA256SUMS.txtis unchanged. Details:docs/v2/internals/desktop-packaging.md.
Upgrade notes for v0.0.51
- A session started before this version keeps the pictures it imported. A new session imports the WebP files. A world of your own that ships PNG portraits or scenes still works: PNG remains an accepted format, and the
emit-presence/emit-scenesscripts read both.
[0.0.50] - 2026-10-11
This release makes long sessions cheaper and safer to run and gives characters aliases. Prompts keep a stable prefix that a provider's cache can serve, per-turn reads no longer grow with the length of a session, a form answer and its follow-up turn are one request, plugin HTML runs on its own origin, and start-up refuses a mistyped backend or a second process on one SQLite file. A world can declare a dimension value as derived from its clock, and the value is then computed in code instead of by a model. Development data is not carried over: an SQLite or PostgreSQL database created by an earlier version must be recreated, because the characters table has a new column and the server never alters a table. Read the breaking changes and the upgrade notes before installing. macOS Apple Silicon and Windows x64 binaries are unsigned; the macOS build is not notarized.
Verified for this release: the macOS build was started in a separate profile and a session was played in it with a model behind a local relay; the session screen, character cards, portraits, settings, memory and the world dimensions panel were looked at in a browser. Not verified: nobody played a session in the Windows build (it is built and packaged by the release workflow), the screens were not looked at in a browser other than Chromium, and no request went to a first-party OpenAI, Anthropic or Google endpoint; model tests ran through a local relay. A world author who does not want a later-revealed name shown should not declare it as an alias: the character panel lists aliases.
Breaking changes in v0.0.50
- The app's page runs no inline script and frames only its own origin. The page policy is now
script-src 'self'(it had'unsafe-inline') withframe-src 'self'. A self-hosted deployment whose reverse proxy or static host injects an inline<script>into the page (an analytics snippet, a banner) must load it from a file on the app's origin instead, or it will not run. The build has a new file,/plugin-frame.html, which a static host must serve as that file and not as the single-page fallback; a host that copiesdist/webwhole needs no change. Plugin authors change nothing: awebviewdocument keeps inline scripts, inline event attributes and inline styles. GET /api/worldsreturns summaries. Each item has noloreand nodimensions, and itsmetadatakeeps onlysource,storage,packageManaged,generated,cover,accentColor,supportedLocales,packageInfo,defaultViewMode,pluginSettingsand thename/descriptionentries oflocalizedText. The list of the four bundled worlds shrinks from about 267 KB to 3.9 KB. A client that read the lore, the dimensions or another metadata key (embeddedCharacters,characterSchema,pluginPolicy,worldData) from the list must callGET /api/worlds/:id, which still returns the whole record. In the web client the session store and the world list hold summaries; the session-prep screen, the world page, the world editor and the World tab read the full record when they open it. The first visit of the browser-private profile now reads each catalog world withGET /api/worlds/:id. TheWorldSummarytype,worldSummarySchemaandsummarizeWorldare in@covel/shared. Rules:docs/v2/reference/api.md.- Diagnostic traces older than 30 days are deleted.
COVEL_TRACE_RETENTION_DAYSnow defaults to30instead of0, so after the upgrade the server removes the runtime trace events (model requests and replies, tool calls, usage) of every session that are older than 30 days, once at start and then once a day, and after each commit for the session that committed. Stories, messages and saves are untouched; only what the debug page can show shrinks. To keep everything, setCOVEL_TRACE_RETENTION_DAYS=0or choose "Keep everything" in Settings (selftier, desktop app or self-hosted web). A set variable wins over the Settings choice;demo/commercialfollow the variable alone. - An image part of a model message is
{ type: "image", image, mediaType? }.imageis a string (base64, adata:URL or anhttp(s)URL), as in the Vercel AI SDK; the earlier{ type: "image", image: { url, mimeType } }(plugin SDKPluginTextMessage) and{ type: "image", image: MediaRef }(@covel/sharedLLMImagePart,@covel/ai-providerImagePart) are gone. A plugin that sends an image throughctx.gatewaymust pass the new shape. A target known to take text only now has image parts removed; theimage_refJSON text that stood in for an image without a URL no longer exists. assetGenerateToLLM,assetGenerateViewToLLMand theContentParttypes left@covel/shared. Nothing called them.pictureOfandpicturesShownread a picture and its caption from a stored block instead.- A picture's
meta.captionormeta.promptis shown to the story model. An image plugin that put text there which the story model must not read has to move it to anothermetafield. - A history window counts player turns only.
agent.history.maxTurnsno longer counts asystemconversation row (a picture note) as a turn of its own. - Pictures from before this change have no note. A development session keeps its pictures on screen, but the story model learns only of pictures generated from now on; nothing is migrated.
- The OpenAI Responses protocol no longer lets the provider keep the conversation. Every request of a slot with
protocol = "openai-responses-v1"now sendsstore: false; before, the field was sent only when the slot set it, and OpenAI keeps responses for a time by default. A slot that relied on stored responses must setstore = truein itsproviderOptions: one that passesprevious_response_iditself throughextraBody(Covel never sends it), or an operator who reads responses in the provider's dashboard. No stored data changes. Rules:docs/v2/reference/slots.md. branch-replytakes no narrative text from a request. The manual payload no longer readsbaseText,candidates(oncreateCandidates) ortext(onacceptCandidate): candidates are the turn's stored narration and the rephrasings the plugin generates from it, andcreateCandidatesfails for a turn that has no stored candidate set. An adopted reply replaces the turn's narration in the model's history, so a request could rewrite a turn while the items, quests and affinity taken from the real narration stayed.character-blueprinthas noimportruntime andliving-world-ruleshas no runtime. Both wrote game content during play (a character card, a world rule) through plugin-rpc with no panel that called them. Cards and rules come from the world package (character.blueprints@1,world.rules@1). A script that calledcharacter-blueprint/importor theliving-world-rulesmanual runtime must put the record in the world package instead.scene-stagekeeps dialogue attribution in one row.stage.directionwritesdialogue/currentinstead ofdialogue/<turnId>. A development session shows no speaker names for its last turn until the nextstage.directionevent.- A scene matches by each of its names.
locationRefin a world'smedia/scenes.jsonis read as/-separated names, not as one string. A location shows a scene when it equals a name, then when it contains one (the longest wins), then when it is a fragment of a name that only one scene has. A location that is a fragment shared by two scenes no longer shows the first of them. Rules:docs/v2/reference/world-data.md. - A tabletop check converts the attribute over its range.
tabletop-rulesaddsround((value - min) / (max - min) * 10)to the d20, asdice-checkdoes, instead of the raw attribute value; the receipt has the value asscore. An attribute of 4 on a 0-5 scale now gives +8, and one of 14 on a 3-18 scale gives +7 instead of +14. chat-mode-narratorrequires only the stage contracts.branch-reply@1,living-world-rules@1,character-blueprint@1andcharacter-presence@1left itsrequires, so a session no longer pulls those plugins in because of the narrator. The dialogue pack and the bundled worlds request them by name; a world or pack of your own that relied on the narrator to bring them must list them.- A world translation file named with only a language is not read.
WORLD.en.md,world.en.yamlordata/x.zh.yamlin a world that declaresen-US/zh-CNis ignored (with a server log line) and rejected when the world is installed or imported. An author renames the file to the declared locale (WORLD.en-US.md);pnpm validate:worldnames the new file name. - An agent writes through its tools only.
statePatches,pluginData,notifications,assetGenerations,interactions,uiand events with atopicin an agent's final JSON no longer become effects; they are removed and the server log names the runtime and the fields. Before, they were committed without passingagent.toolsor the runtime's declaredeffects, so an agent that was allowed only read tools could still write session state. No bundled agent used the channel. A plugin whose agent wrote this way must give the agent a tool for the write (plugin-data-set,emit-event,create-notification, a plugin tool) and list it inagent.tools.preGameDone/completionin the final JSON still report completion, and events without atopicstay in the output. - A hook finds the runtime in its context, not in the payload.
ctx.pluginIdandctx.runtimeIdname the runtime an event is about for every runtime event (PreRuntime,PostContextAssembly,PreLLMCall,PostLLMResponse,PreToolUse,PostToolUse,PostRuntime,PreStateCommit,PostStateCommit). The payloads ofPostContextAssembly,PreLLMCall,PostLLMResponseandPreToolUseno longer carrypluginIdandruntimeId, and thePostContextAssemblypayload no longer carrieslocale. A hook that readspayload.runtimeIdorpayload.pluginIdmust readctx.runtimeIdorctx.pluginId; with the old name it matches no runtime and does nothing.createNarrativeReviewof the SDK reads the context in the same way. - A concealed runtime's text stays out of the conversation. Text that a runtime with
io.concealed: truereturns (narrativeOutputorcontent) is no longer written to the conversation log, so it does not reach other runtimes' prompt history,GET /messagesor the rebuilt prompt. Its forms and UI blocks are written as before. A manifest that declares bothio.concealed: trueandio.visibility: storynow fails to load: a story runtime's text is what the player reads. A plugin that relied on a concealed runtime's text appearing in the chat must hand the result over throughio.outputor an effect. - One session has one declarer of an event topic. When two plugins of a session declare the same topic in
contributes.events, the one accepted later is left out of the session with the rejectionevent-topic-conflict, and the reason names the topic and both plugins. Before, both were active and every event of the topic was validated against the schema of the plugin whose ID sorts first, with one warning in the server log. Both plugins can stay installed; a player picks one per session. A plugin author avoids the collision with a topic prefix of the plugin's own.GET /api/pluginsand the session plugin list returneventTopicsfor each plugin. ctx.pluginDatareads have one shape in every context. In an extension-point handler,ctx.pluginData.get(namespace, key)now returns the stored value, ornullwhen there is none, as it does in a function runtime and a guard; before, it returned the whole row orundefined.list(namespace)returns{ key, value, createdAt, updatedAt }entries in both contexts: an extension handler no longer getspluginId,namespace,sessionIdandidon a row, and a function handler now gets the two timestamps. A plugin whose extension handler reads(await ctx.pluginData.get(ns, key))?.valuemust drop.value. The SDK typeExtensionPluginDataRecordis replaced byPluginDataEntryandPluginDataReader.- An interaction is answered once, and its follow-up turn runs once.
submit-formrefuses a second submission of the same(turnId, interactionId)with400andcode: "interaction_already_submitted"as soon as a turn has started after the answer was stored (a successful turn, or a failed one that the existing retry runs again). Before, the same values were accepted again and returned the firstsubmissionId, so a second tab or a repeated click ran the follow-up turn twice. While no turn has followed the answer (the response was lost, or the browser closed between the answer and the follow-up) the same values are still accepted and return the storedsubmissionId, so the client can start the follow-up; different values are refused. The text of the response is written for the player in the session's language. A script that resent a submission after its turn ran, to read itssubmissionIdagain, must read the stored answer fromGET /api/sessions/:id/viewinstead. - One plugin-data value is at most 256 KiB. A
plugin.data/plugin.data.batchproposal,ctx.pluginData.setor an RPC store write whose value is over 256 KiB of JSON (UTF-8) fails with an error that names the plugin,namespace/key, the size and the limit; the stored value stays as it was and nothing is truncated. The largest seeded record in the bundled worlds is about 7 KB, so no bundled plugin is near it. A plugin that keeps a growing list under one key must split it across keys or trim it. - The
characterstable has analiasescolumn; an earlier SQLite or PostgreSQL database must be recreated. The server creates tables and never alters one, so a database created before this change opens without a message and then fails every character read and write withno such column: aliases(SQLite) orcolumn "aliases" does not exist(PostgreSQL). Stop the server, back up the database file with its-waland-shmfiles, and start with a new file (<covelHome>/data/covel.dbfor the desktop app,./data/covel.dbfor a development server without a Covel home); for PostgreSQL recreate the schema. Browser-private saves need nothing: their characters have no aliases. update-characterreports a miss with other words. The error is nowCharacter "<what was passed>" not found. Closest known names: …(orCharacters in session: …), notCharacter <id> not found in session. …. A plugin or test that matched the old sentence must matchnot found.- A world whose characters share a name or alias does not validate. In
characters/characters.json, analiasesentry that equals another character's name or alias, or a name that equals another character's alias (letter case, width, spacing and the middle dot do not count), is an error ofpnpm validate:worldat the later record, and a session of such a world is not created. A generated world stored before this change whose character cards repeat an alias across characters fails the same way: edit the alias or generate the world again. affinity,npc-graphandcodexkey a session character's record by the character's name. A change addressed by an alias of a session character now goes to the record under the character's name. A development session that already has a second record under the alias keeps it; it is no longer written to.- One plugin-data value is capped at 256 KiB on every write path.
PUT /api/sessions/:id/plugin-data/...used to refuse values over 64 KB and now refuses values over 256 KiB (HTTP 413); world-packageworldDatarecords over 256 KiB now fail the import with an error diagnostic instead of being stored. Split an oversized record into several records or keys. - A form is answered through
POST /api/actions, not through plugin-rpc. The framework actionframework.submit-formonPOST /api/sessions/:id/plugin-rpcis gone (it now returns404 unknown_action), and so is the second request that sent the filled narrative as asend_message. A client sends one action,{ type: "submit_interaction", payload: { turnId, submissions } }, and reads the answer and the follow-up turn from that one SSE stream. Contract:docs/v2/reference/api.md, the player interaction section. - A refused answer arrives on the stream, not as HTTP 400.
form_rejected(withdetails.issues),interaction_already_submittedandinvalid_interaction_submissionare thecodeof a singleerror.occurredevent on a200stream that stores nothing and starts no turn. A request for the form provider's code grant is still a202JSON response before the stream opens. - Submitting the same answer again is refused, whatever its values. The server no longer accepts a resubmit of identical values to hand the stored narrative back: an answered interaction always has its turn, so a client that lost the response reads
submittedInteractionsandGET /api/sessions/:id/executioninstead of sending again. submittedInteractions[].followedUpis removed from the session view. An entry in that list always has a turn after it. A development session that holds an answer stored by an earlier build with no turn after it now shows that form as answered; send any message to continue it, or recreate the session.@covel/runtimeno longer exportscreateSubmitFormHandler.createInteractionSubmittervalidates an answer and returns the rows for the caller to store in its own transaction.- The player cannot take a name or an alias of a character of the world. The character form refuses it at the name field (
form_rejectedwith an issue oncharacterName); before, a name equal to another character's full name was accepted and made a second character of that name.create-characterwithtype: "player"now fails for such a name, where it returned the other character as "already exists" (alias) or created the duplicate (full name). A plugin that creates the player throughcreate-charactermust ask the player for another name when the call fails. Existing sessions are not changed. - Sessions created before dimension values held one language no longer open. The read-time repair that rewrote locale-map dimension records is gone, together with
resolveDimensionRecordLocaleand the_legacy.dimension-locale-mapsbackup namespace. Recreate such development sessions (this release already requires recreating existing databases). - An unknown
STORE_BACKEND,MEDIA_BACKENDorVECTOR_BACKENDstops the server from starting. Before, a typo such asSTORE_BACKEND=postgressilently ran on a local SQLite file. The error names the variable and the accepted values; fix the value in.env, the shell or the desktop config.NODE_ENVkeeps its fallback. Seedocs/v2/reference/env-registry.md. GET /api/sessions/:id/viewreturns only the answers to the turns of its message window.submittedInteractionsheld every answer of the session and grew with each choice; it now holds the answers whoseturnIdbelongs to a returned message (the message'sturnIdor its form block'smeta.turnId). The web client marks only blocks of that window, so it shows the same. A client that looked up an older turn's answer in this list no longer finds it. Seedocs/v2/reference/api.md.DataStore.listTurnMessagestakes{ turnId }. The second argument isPaginationOpts & { turnId? }; withturnIdonly that turn's messages are returned. ADataStoreimplementation outside this repository must honour the option, or a form answer is checked against the wrong messages. The three built-in backends do, under a shared contract test.- Five SSE event types that nothing emitted are removed from
CovelEvent:interaction.completed,state.snapshot,record.updated,connection.restoredandmemory.updated. A client that listed them must stop handling them; no server code ever sent them. - A plugin action can no longer write a player submission, and its turn-message read is bounded.
savePlayerInputis removed from the store view ofregisterRpchandlers (PluginRpcStorein@covel/plugin-handlers-utils), so an action cannot store input that skips form validation for another plugin to trust.listTurnMessages(limit?)there now returns the most recent committed messages, at most 200, instead of the whole history; usereadTurnMessagesin a function runtime for paging. No bundled plugin used either. A community plugin that calledsavePlayerInputmust submit through the form flow instead. Seedocs/v2/reference/api.md. - Emberback's Crownfire countdown has a new shape, so an Emberback development session made before this change must be created again.
crownfire.stageno longer has the memberpassing; the dimension has a new required fieldfrontPassed(boolean) that the tracker sets when the narrative says the front has moved on.minutesRemainingandstageare computed from the station clock. A session keeps the definition it was created with, and that definition no longer matches the world. World version 1.2.0. - What
ctx.worldreturns is frozen.ctx.world.characters,characterSchema,dimensionsandworldRecordwere a new deep copy on every read: 2.4 ms for a cast of 200, paid again by each step of a loop that looks one character up. A read now returns one frozen copy until the execution buffers another write (the same loop: 494 ms before, under 1 ms after). A plugin that sorted or changed the returned value in place now gets aTypeError; copy first ([...ctx.world.characters],structuredClone).docs/v2/reference/plugins.md. - The two unpaged trace endpoints return the newest 5000 events.
GET /api/traces/:sessionIdandGET /api/traces/:sessionId/turnsread every trace event of the session into memory, whole prompts included. They now read at most 5000 events, returnnextCursorwhen older ones remain, accept?cursorto read those, and senddiscoveryonly on the request without a cursor. A client that needs everything follows the cursor (the debug page's "Load all" andpnpm e2e:verifydo).docs/v2/reference/api.md. plugin-data.changedcarriespluginIdandchangesonly. Thesourcefield is gone; no client read it. Each change still carries the row's public value, because the client writes it into its own copy and reads nothing back.docs/v2/reference/protocol.md.- A resolved suspension no longer holds its prompt. A suspension stored the whole request it continues, and kept it after the resume committed, so a session carried one prompt for every question the player had ever answered, in the database and in every browser checkpoint.
markSuspensionResolvednow keeps the execution identity, locale and turn number and drops the rest. Code that readpendingContinuation.messagesof a resolved record finds an empty list. Resolved rows written by an earlier version keep their content.docs/v2/internals/storage.md. DataStore.claimSuspensionreturns the claim marker, and a claim is released withreleaseSuspensionClaim.claimSuspension(id)now resolves to the marker it wrote (a string) ornull, where it resolved totrue/false. The newreleaseSuspensionClaim(id, claim)reopens the suspension only while that marker is still in place. A customDataStoremust implement both; stored data is unchanged and no session has to be recreated.- The context budget result reports
overBudget.applyBudgetin@covel/contextreturnedbudgetExceeded, which was true both when history was pruned to fit and when the prompt was still too large after pruning. It now returnsoverBudget, true only when the prompt is still over the limit;prunedCountsays whether anything was pruned. The assembled context no longer carriesbudgetExceeded. - The rules segment of
world-initis nameddimension-rules-truncatedwhen it leaves rules out. It keeps the iddimension-ruleswhen every rule fits. Code that looks the segment up by id must accept both. - The default first-token wait of a streamed agent call is shortened to leave room for the retries. An agent that does not set
agent.loop.firstTokenTimeoutMsno longer waits 120 seconds on every attempt: the last attempt may wait for all that is left oftimeoutMs, and an earlier one for its equal share of it, or 60 seconds when the share is shorter, never more than half and never more than 120 seconds. WithtimeoutMs: 120000andmaxRetries: 3the attempts wait 60, 30, 15 and 15 seconds. A runtime whose model sends its first output (text, reasoning or a tool call) later than that must setfirstTokenTimeoutMs, which is used as written for every attempt, and atimeoutMsthat fits it. Rule:docs/v2/reference/plugins.md. - A dimension settlement receipt no longer has
blockedBy. A session that holds a pending receipt written with it (a turn whose shared extraction failed, on an earlier build) cannot be read: recreate that session.
Added
- Settings has a page for stored media. Settings → Stored media lists the pictures, audio and video the app has stored (generated by plugins, uploaded, or imported from a world for a session), with the size, the date and the sessions that use each item, and shows how much space the items that no session uses take. The player deletes chosen items or everything unused after a confirmation that states the count and the size; an item that a session still uses is deleted one at a time, after a confirmation that names the sessions, and those sessions then show a "media unavailable" placeholder in its place. Nothing is deleted without that action, and files inside world packages are never touched. Served by
GET /api/media/libraryandPOST /api/media/library/delete, on a self-hosted server and the desktop app only; hosted tiers answer503.MediaLifecyclePolicygainsonlyIds. Seedocs/v2/reference/media-store.md. - A self-hosted web deployment has server-side settings. A setting the server itself acts on is now kept by the server, in its database, and can be changed in Settings on a self-hosted web deployment as well as in the desktop app; every browser of the install sees the same value. The registry marks such a setting with
scope: "server"; the first one is "Keep diagnostic traces".GET /api/config/server-settingsreports each value in force, its source and whether it can be set, andPUTstores a change, which applies to the next commit or sweep without a restart. Writes are accepted onDEPLOYMENT_TIER=selfonly and checked against the list of known keys and their allowed values; ondemo/commercialthe operator sets these through the environment and the control is locked. The environment variable still wins over a stored value. The control stays disabled until the server has answered, and a write the server refuses shows the reason and puts the earlier value back. A newserver_settingstable is created at start on SQLite and PostgreSQL. Seedocs/v2/reference/settings-store.md. - Trace retention is a player setting, and a sweep reaches idle sessions. Settings → General has "Keep diagnostic traces" (7 / 30 / 90 days or keep everything) with a plain-words explanation; on a deployment that fixes the period, or on a
demo/commercialserver, the field shows the value in force and is disabled. The server also sweeps every session at start and once a day, so traces of a session nobody plays again are deleted too. A new SQLite database is created with incremental auto-vacuum and the trace delete hands pages back to the file; a database created earlier keeps its size (freed pages are reused, seedocs/v2/internals/storage.md). Precedence:docs/v2/reference/env-registry.md. - OpenAI requests carry a prompt cache key. Every model request of an agent runtime sends
prompt_cache_keyon the OpenAI Chat and Responses protocols: a hash of the session and the runtime, equal for the requests that share a prefix. OpenAI routes by it, so a runtime's request on the next turn reaches the machine that holds the cached prefix. The field goes toapi.openai.comonly; a relay that forwards it is switched on withpromptCacheKey = truein the slot'sproviderOptions, andfalseturns it off. Seedocs/v2/reference/slots.md. - The story model knows which pictures the player saw, and a model that takes images sees them. When a picture is committed, the conversation history gets one line at that point: a picture was shown, and what it depicts (
meta.caption, elsemeta.prompt; no IDs, paths or timestamps). Every model that reads the history gets it. A story agent whose model slot is known to accept image input also receives the newest 2 pictures themselves, as inline image parts read from the media store (session-checked, PNG/JPEG/WebP/GIF, at most 3,750,000 bytes each). They sit in the per-turn part of the request, so the cached history prefix does not change when a picture ages out; the cost is about 1,500 input tokens per picture on each story call. Every protocol adapter (OpenAI Chat, OpenAI Responses, Anthropic, Gemini) encodes inline image data, and traces record its length instead of the bytes. Seedocs/v2/reference/prompt-structure.md. - A world's translation files have one spelling, in the game as in the validator. A file such as
WORLD.en-US.md,world.en-US.yamlordata/items.en-US.yamlis read for a session when its locale is written exactly as the world'ssupportedLocaleswrites it. A file that names only the language (WORLD.en.md) is no longer read through a language fallback; the server log names it and the file it should be called,pnpm validate:worldreports it as before, and installing or importing a community world with such a file fails with the same message. Fallback between editions the world has (azh-TWsession reading thezh-CNedition) is unchanged. Plugins (locales/<locale>.yaml) and prompt bodies (*.zh.md) keep their own rules. - A refused form shows each message under its field. The
form_rejectedresponse now carriesdetails.issues({ field?, message }), one entry for every field that failed instead of only the first. The form in the message list shows each message under its field, focuses the first one and keeps a summary above the fields for errors that belong to no field; the "Check the form" toast is gone. A plugin form validator may return a string (form-level, as before),{ field, message }or a list of them (PluginFormIssuein the SDK). Theerrortext of the response is all messages joined by newlines. - A form's
noticeis a declared field.create-formtakes an optionalnotice(the note above the fields when a form is offered again), and the SDK exportsPluginFormInteractionfor a form sent througheffects.interactions.char-creatornow passes it through the tool instead of patching the returned object. - History compaction keeps a new summary when the merge of older ones fails. The model call that wrote the new summary is no longer thrown away; the provider returns it with
deferred: { reason }(new optional field ofhistory.compact@2), the reason is recorded ascontext.compaction.failed, and the merge is tried again on a later turn. Meanwhile the summaries may exceed their budget, up to twice the segment and token limits. pnpm prompt:prefix <covel.db>shows what a prompt cache can serve. For each runtime of a recorded session it lists the size of a turn's first model request, how much of it repeats the previous turn's from the first character, and the message and offset where the two first differ. It reads the recorded requests intrace_eventsand needs no model. Seedocs/v2/reference/prompt-structure.md.- The world page says when the lore does not fit. A world whose
WORLD.md(narrator-only parts included) is over the 8,000-token budget of the story prompt shows a notice with the estimated size. The budget and its measure now live in@covel/shared(fitWorldLore), shared by the prompt,pnpm validate:worldand the page. - A world declares its own cover and accent colour.
world.yamltakescover(an image one directory undermedia/) andaccentColor(#rrggbboroklch(L C H)). The world card, the world page, the preparation screen and the session backdrop show what the world declares; a world without them gets the app's default picture and a hue derived from its ID.pnpm validate:worldrejects acoverthat names a file the server cannot show. The four bundled worlds declare theirs. - A hook knows the session's language. Every hook context has
locale, the session's content locale, for the turn, commit and session events alike. A hook that adds text to a prompt picks its language withinstructionLocaleFor(ctx.locale); the bundled hooks no longer guess it from the prompt text. docs/v2/reference/hooks.mddescribes every hook event: when it fires, its payload, what a handler may return, how failures are handled and what the context holds. A test fails when an event is missing from the page.- Fonts ship with the app, and the app contacts no font host at start. Inter, Fraunces, Newsreader, Geist and Geist Mono (Latin subset, SIL OFL 1.1, from
@fontsource-variable/*) are bundled; the render-blocking Google Fonts stylesheet and its preconnects are gone, so the first paint no longer waits for a host that is unreachable in some regions, and the desktop app makes no third-party request at launch. Syne, which no theme used, is dropped. The added files are about 530 KB, loaded only for the faces a theme uses. An imported theme's own remote CSS still loads. - Images from other websites in the story or a world document wait for the player. Images the app serves itself and
data:/blob:images load as before; an image on another origin shows a placeholder naming the host, with "Load this image" and "Always load images from this host in this world" (kept per world in the settings store). The built page also carries a Content-Security-Policy (docs/v2/internals/security.md). - Memory search no longer depends on an embedding model for good results. Without an embedding slot,
memory-searchranks with BM25 instead of counting shared words: a name or an item outweighs a word that every passage holds, and a long passage is not put last for being long. On a synthetic 60-message Chinese session with paragraph-length narration, the first result was the right one for 11 of 12 questions, against 6 before. An embedding slot stays optional and is used when present. memory-searchreads the history summaries. Recall covers the summaries that history compaction wrote as well as the latest 500 messages, so a turn that is older than those messages can still be found. A summary result has the rolesummary.- A plugin can make a data namespace searchable.
contributes.data.<namespace>.search: { text: <field> }puts the records of the namespace intomemory-search(sourcearchival:plugin_data) while the plugin is active in the session, and into the vector index when an embedding model is configured. A namespace without the declaration is not read. - The plugin SDK exports the ranking the kernel uses.
rankTexts,searchTermsandsearchExcerptfrom@covel/plugin-handlers-utilsrank a plugin's own texts against a query with the same BM25 implementation, with no model call. - The story remembers what happened, not only how things are now. The
memoryplugin adds up to three facts of each turn to afactsnamespace that is never rewritten.memory-searchfinds them, and each story prompt brings back the earlier facts that the player's message is about. - OpenAI Decisions is an evaluation protocol. A model with
protocol = "openai-decisions-v1"(todaygpt-6-luna) answersgateway.evaluate()andctx.gateway.evaluate()throughPOST /v1/decisions. The settings page offers it under Evaluation and preselects it for theopenaiprovider. The public question and answer types do not change: a boolean is sent as apredicate, choices and score levels as lists, and the answers come back keyed by question. A question the model declines fails the call as a refusal. Seedocs/v2/reference/evaluation.md. - Nine more providers can be picked by name. xAI, Mistral AI, Groq, Together AI, Fireworks AI, Moonshot AI, Zhipu AI, SiliconFlow and Volcengine Ark join the built-in providers. Settings → Providers & Models → Add provider has a provider list that fills in the ID and the official endpoint; "Custom provider" keeps the manual fields. The endpoints are in
docs/v2/reference/slots.md. - A plugin can provide a text protocol.
covel.registerWires({ text: [{ id, generateText, streamText }] })registers a request and response format beside the four built-in ones. A model whoseprotocolis<pluginId>/<wireId>uses it for every text call, the story model included;llm.tomland the settings page accept the ID, and the protocol list of the settings page shows it (GET /api/ai/protocols). Object calls work throughgenerateText, and an optionallistModelsfeeds the model list. Seedocs/v2/reference/plugin-extensions.md. ctx.gateway.generateTextis typed forproviderOptions. The runtime already passed it on; the plugin SDK type did not have the field.- Services on this machine can be picked by name and need no key. Ollama, LM Studio, llama.cpp server, vLLM and LiteLLM Proxy are in the provider list at their default ports; a local gateway on another port, or one that serves several protocols, is a custom provider at
http://127.0.0.1:<port>/v1. A model on a loopback address counts as ready without an API key, so it can start a session, run background tasks and be tested. Before, such a model was shown as not configured until a placeholder key was entered. - The settings page reads a service's own model list. "Read the model list from the service" in the add-provider and add-model dialogs asks the endpoint for its model IDs (
POST /api/ai/models); a tick adds one, a filter narrows the list, and IDs typed by hand stay. - A failed connection test says what kind of failure it was.
POST /api/ai/pingand the model list returnerrorKind(unreachable,timeout,auth,quota,rate_limited,not_found,bad_request,overloaded,server,refused,config) and the settings page shows what to do about it. A refused connection names the host and port instead offetch failed. - An
llm.tomlslot of a built-in provider needs onlyproviderandmodel.baseUrlandprotocolcome from the provider when the slot omits them. For any other providerprotocoldefaults toopenai-chat-v1andbaseUrlstays required. - A world can keep notes for the narrator out of the player's view. In
WORLD.md, the lines between<!-- narrator-only -->and<!-- /narrator-only -->go to the model and are not shown in the World tab, on the session preparation screen or on the world page. It keeps spoilers out of sight and is not secrecy: the files and the API hold the whole text.pnpm validate:worldreports a marker that is not closed. The four bundled worlds use it for hidden plotlines and game-master notes, and world generation writes such parts this way. - Character cards reach the story. The
character-blueprintplugin gives the narrative each card's voice, traits, goals, fears, secrets (for the narrator only), relationships, rules and example lines as a prompt segment, for the characters that are in the session, within a budget of 6000 estimated tokens. Before, only the panel read the cards. World generation writes preset characters by default and no longer asks for character fields that had no effect. - A wrong alias can be taken away.
update-characterandsync-characters.updates[]acceptremoveAliases, and acharacter.upsertproposal withexpectedVersionreads the same field. Names are compared as everywhere else (case, width, spacing); a name that is not an alias of the character changes nothing, and the character's own name stays. An alias-conflict error now says that the other character's alias can be removed when it is wrong. There is no editor for it in the app. - A character has aliases, and one rule turns a name into a character. A story calls one person many things; the models that track characters, relationships and affinity looked a person up by one exact name, and a miss made a second character, a relationship on the wrong one or a lost update. A character of the World Model now has
aliases(ordered, as shown). A world author writes them incharacters/characters.json; the bundled worlds carry the ones their character cards already listed. A model adds one throughaliasesofupdate-character/sync-characterswhen the story reveals a name, andcreate-charactertakes them too.resolveCharacterin@covel/plugin-handlers-utilsis the one lookup: exact ID, then name, then alias, compared without regard to letter case, full and half width, spacing, surrounding quotation marks, the middle dot of a transliterated name, the kind of apostrophe and a leading "the"; titles and honorifics are not removed, and there is no fuzzy match. A name that two characters have is reported as ambiguous. An alias that belongs to another character is refused, by the tools and again at commit, with that character named.update-characteraccepts the id, the name or an alias; a miss names the closest known characters.create-characterreturns the existing character for a name that is an alias.get-characterandlist-charactersshow aliases.affinity,npc-graph,codex,scene-stageandworld-irresolve names through it, and the character panel shows aliases (read-only). In prompts a name with aliases readsName (aka a, b): 5 to 9 tokens per character on the bundled worlds. Seedocs/v2/reference/world-model.md. - A post-turn runtime can read the time settled for the current turn.
world-time/advancepublishes the contractworld-time-evolution@1with a schema. Itssummaryholds the display text, the base unit, the time elapsed since the session started and the time elapsed in this turn. Bind it as an optional input withselect: /summary; the binding also orders the consumer after the time settlement. Reference:docs/v2/reference/world-time.md. For world authors: write theupdateRuleof a time-bound dimension as a function of the clock (docs/v2/reference/dynamic-dimensions.md). interaction.submittedSSE event. It is the first event of asubmit_interactionstream: the values the server stored, each filled narrative, and the id and text of the player message the follow-up turn carries. Payload:docs/v2/reference/protocol.md.lateSystemAsUsermakes the story model's prompt cache reach the end of the previous turn's history on a relay that moves system messages. Off by default. The turn's data and rules are a system message placed after the history; a relay that moves system messages to the front (the local Codex-style proxy used for tests does) puts that changing text ahead of the history, so the narrator read only the relay's own fixed instructions from cache. WithlateSystemAsUser = truein a slot'sproviderOptionsonopenai-chat-v1oropenai-responses-v1, a system message after the first conversation message goes out as ausermessage in<system-instruction>tags, in the same place and with the same text. On that proxy, in an eight-turn session, the narrator's first request of a turn read 55% of its input from cache instead of 22%. It changes the role the model sees; other servers were not measured. Seedocs/v2/reference/slots.md.- Settings says when the environment overrides a saved provider key. On desktop, a provider key in the shell,
.envor.env.llmoutranks the key saved in Settings.GET /api/llm-confignow lists such providers inenvKeyOverrides, and the API keys page shows a notice under the key. Precedence is unchanged (docs/v2/reference/api.md). - A new model's reasoning levels can be added without a release. A
reasoning-models.jsonin the user configuration directory (~/.covel/on desktop,COVEL_USER_CONFIG_DIR) lists model name patterns and the levels they take; the server reads it at start, before the bundled entries. An invalid file is reported in the log and ignored. Format and an example: the reasoning level data section ofdocs/v2/reference/slots.md. - One request restores all plugin data of a session.
GET /api/sessions/:id/plugin-datareturns the readable rows of every active plugin with the same visibility rules as the per-plugin and per-namespace reads; the web client uses it for reconnect, return to the tab and reset instead of one request per active plugin. Seedocs/v2/reference/api.md. - Releases attach a
SHA256SUMS.txtfor the installers. The release workflow lists the SHA-256 of every.dmg,.zipand.exe, so an unsigned download can be checked against it. Seedocs/v2/internals/desktop-packaging.md. - A world can declare that a dimension field is computed from the world clock, and code then computes it every turn. A schema node takes
x-derive:source: clock.elapsedSinceStart, a linear form (start,perUnit, optionalmin/max), and for a label an ordered list ofranges. The new function runtimeworld-init/dimension-clockreads the settled clock throughworld-time-evolution@1, after the time settlement and the tracker, and commits the value with a versioneddimension.update, in the same turn and without a model call. The value depends on the clock only, so a turn that is run again or a session forked from an earlier turn has the value of its clock.pnpm validate:worldchecks the declaration: where it may stand, the type, that every label is a value the field allows, that the bounds lie inside the field'sminimum/maximum, and thatinitialValueis the value at the start. Reference:docs/v2/reference/dynamic-dimensions.md. - The SDK exports the derivation helpers.
@covel/plugin-handlers-utils/dimensionshasapplyDimensionDerivations,derivedDimensionFields,deriveDimensionValue,keepDerivedDimensionFields,dimensionSchemaWithoutDerivedanddimensionValueWithoutDerived. A schema in aworld.dimensions@1snapshot can carryx-derive. compareTextin the plugin SDK and@covel/shared. Compares two strings by Unicode code point, the order SQLite and PostgreSQL give a binary-collated column. Use it to sort IDs, keys and timestamps and to break ties.docs/v2/plugins/essentials.md.- The server log names an in-process session lock wait that passes 30 seconds. The lock used by the Memory and SQLite backends still waits (a turn holds its session for the whole model run, and a request queued behind it must run when it ends), and now writes one line when a wait is that long: the first sign of an owner that never returns.
- Context and model-call hooks are told which prompt segments were used. The
PostContextAssemblyandPreLLMCallpayloads carrypromptSegments: theprompt.segment@1segments the runtime's prompt was built with, each as{ pluginId, id }. A plugin reads it to learn what it put in the prompt and no longer has to search the prompt text. Reference:docs/v2/reference/hooks.md. ctx.gateway.generateTexttakesresponseFormat. A function runtime passes{ type: "json_schema", schema }to ask for a JSON reply: the provider's JSON mode is used where there is one, and the schema is added to the instructions.textis still text, which the plugin parses and checks.- A plugin's warnings and errors reach the server log.
ctx.logger.warnandctx.logger.errorstill write to the session's_logsring, and now also print one line ([plugin-log] <runtimeId>: <message> <meta>, the detail cut at 2000 characters).
Changed
list-charactersreturns at most 50 characters. The most-interacted come first;countstays the total, and_textends with how many were left out and how to find one (filter bytype, orget-characterby name). Thecharactersarray holds only the listed ones.- Anthropic requests send every message text as a block. With cache breakpoints on, a message's text is always a one-block array instead of a string on some turns and an array on the turn it carries the history breakpoint, so the cached prefix is the same bytes on the next turn.
- A browser-private checkpoint carries only the newest revision of each runtime export, not every revision ever published, so the upload and download at each action stop growing with the number of turns on that part.
- Tools on the Responses protocol are no longer strict by accident. Function tools are sent with
strict: false. The Responses API treats a tool as strict when the field is absent (Chat Completions does the opposite), which makes every optional argument required: the model then filled each one with an empty value (defaultValue: "",min: 0,options: []) that the tool refused. In three-turn sessions through a local proxy, the same world gave 1 and 5 rejected tool calls on Responses (one runtime failed) against 0 on Chat Completions, and 0 on Responses after the change. - A Responses tool loop works without provider-side storage. With
store: false(now the default) the provider cannot find a reasoning item of the previous response by its ID, so Covel asks forreasoning.encrypted_contentwhenever the request has reasoning on, for any model name (before: only when the slot setstore = falseand the name matched GPT-5 / o3 / o4), and sends the items back as they came. A reasoning item that arrived without encrypted content is left out of the next request and the other items go back without their IDs. When a model answers 400 to the request for encrypted reasoning, the request is sent once more without it and the endpoint is remembered for the life of the process. A stream whose final event has an emptyoutputnow keeps the items that arrived one by one; before, the reasoning of that response was lost for the next call. Recorded provider requests in traces showinclude. inventoryandcore-questledgers are plain functions. They were wrapped as model tools (a factory takingtoolandz, parameter descriptions for a model) although only their own function runtime calls them;inventory's unreachablesetoperation is removed.story-eventsreads multi-language text with the SDK'sresolveI18nTextinstead of its own copy.- The two narrative engines have package tests: the stage and contract, every player setting reaching the prompt in both languages, and the same placeholders in the English and Chinese bodies.
- Provider errors carry a type, and nothing decides by their wording. An HTTP failure of a model endpoint is thrown as
AiProviderErrorwhere the response is read (before, it was anErrorwhose message was JSON that the gateway parsed back).@covel/ai-providerexportsOutboundFetchError(no response from the endpoint),SsrfPolicyError,ProviderBaseUrlErrorandWireAlreadyRegisteredError, and marks a request rejected for tool arguments that are not JSON withdetails.requestFault. The retry and fallback rules read these, an error'sstatusCode/status/isRetryable/codefields and the provider's own error code; matching on a message remains only as a marked fallback for an adapter that throws plain errors. A schema failure reads[provider] reply does not match the requested schemainstead of[provider] HTTP ? — …. - The agents that run after the story send less new text each turn. A provider's prompt cache serves a request only as far as it repeats an earlier one from the first character, so the part of a request that does not repeat the previous turn's is what a turn costs at the full price. In three-turn sessions on the bundled worlds that part fell by about a third (Lantern Barrow: 47,000 to 32,000 estimated tokens over three turns; Mistport: 53,000 to 34,000), and the requests are 7–9% smaller. These are measurements of the recorded requests with estimated tokens, not a provider's bill. What changed:
- An input whose whole value is this turn's story text is no longer sent twice. The conversation already carries the text as a message, so the slot in
<runtime-inputs>names that message.ctx.inputsandctx.inputSlotskeep the value. A prompt body that tells the model to readruntime-inputs.<name>.valueneeds no change; a hook that searched the rendered prompt for the story text must readpayload.inputSlots. - The dimension tracker's rules and schemas are in the system prompt (
<dimension-rules>, a session segment) and only the current values change with the turn (<dimension-values>). Neither block carries a version. - The character tracker's roster (
<existing-characters>: id, name, type, description) is a session segment and the current fields are a block of their own (<character-fields>, one line per id). A changed number no longer changes the roster. world-irputs the character roster and the vocabulary ahead of the narrative in its request.affinity,codex,npc-graph/extractorandworld-time/advanceread no earlier turns (agent.history.maxTurns: 0), andchar-creator/character-trackerandworld-init/dimension-trackerread one. They kept two from before the running turn's story became part of the conversation; the player's message and this turn's story are always sent.- The prompt of
chat-mode-narratorno longer carries a note for maintainers, and thenpc-graphprompt no longer describes an update time that its rows do not have.
- An input whose whole value is this turn's story text is no longer sent twice. The conversation already carries the text as a message, so the slot in
- An Anthropic request marks the end of the history for the cache. The one moving breakpoint sat on the last block of a request, after this turn's data, so the next turn could read only the system prompt from the cache and the whole history was written again. A second breakpoint now sits on the last message of the history. This follows the API's documented rules and was not measured against the API.
- Breaking: a world's translation files use the locale exactly as
supportedLocaleswrites it. Bundled worlds mixedWORLD.en.mdandWORLD.en-US.md; they now all use the full tag (WORLD.en-US.md,world.en-US.yaml,data/items.en-US.yaml,media/gallery.en-US.json).pnpm validate:worldreports a translation file named with a bare language (.en,.zh) as an error and names the file it should be called. A community world that still uses.enfiles should rename them; the server still reads them, but the validator rejects the package until it does. - Editing a world's files reaches the app without a restart. The world watcher used to reload a package only when its dimensions changed, and only for
.yamland.mdfiles. It now reloads when any text file of the package changes (theWORLD.mdbody and its language editions,world.yaml,data/,characters/,media/*.json) and updates the world record when anything in it differs. The log says what was reloaded. A new session uses the edited package, and a running session reads the new lore on its next turn unless it has its own lore override. Characters, lorebook entries and dimension values a session already imported are not rewritten. - Breaking: the web app no longer holds a table of world covers. The picture and colour per world ID, and the by-tag fallback to a bundled picture, are gone, as are the unused
cloudmere,neonridge,emberback,haruka-academyandlantern-barrowfiles inapps/web/public/visuals/worlds/. An installed, community or generated world that has nocovernow shows the default background. Addcoverto such a world'sworld.yamlto restore a picture. - The raw Database tab is a developer view. It no longer sits among the player's tabs, where it showed every public plugin row, narrator-only character secrets included. Turn on "Developer view" in Settings to see it; the
/debugpage is unchanged. - A plugin form validator receives the session's language: a third argument
{ locale, messages }to pass totranslate, so the text a player reads when a form is refused is translated. - A memory block over its limit is shortened by the model. The
memoryplugin asks for a shorter version of the block and cuts at the end of a sentence only when that fails. Before, the text after the limit was cut off. finishReasonhas one vocabulary for every protocol. The model gateway returnsstop,length,tool_calls,content_filter,errororother, and keeps the provider's own word inrawFinishReason, as the AI SDK does withunifiedandraw. A plugin that comparedctx.gatewayresults with a provider's word (end_turn,max_tokens,tool_use) must compare with the unified value. The runtime and world generation read the same table (unifyFinishReasonin@covel/shared) in place of three separate ones.- Transport retries follow the providers' SDKs. HTTP 408 and 409 are sent again like 429 and 5xx,
x-should-retrydecides when the provider sets it, andretry-after-msis read beforeRetry-After. When the provider asks for a wait of more than 60 seconds, or more than the call has left, the call does not wait: it ends as rate limited and can use a backup model. Before, it waited until its time ran out and ended without a backup. - A connection test is one request. It no longer retries, so its result and latency are those of the request it sent. An embedding model is tested with a vector request; before, it was sent a text request and reported as failed.
- A protocol is declared in two places instead of thirteen files. The protocol IDs, their names in the settings UI and whether they produce text or evaluations come from one table in
@covel/shared; a protocol's reasoning fields, provider options, optional parameters and media-wire rule are fields of itsProtocolDefinition.docs/v2/reference/slots.mdhas the steps for adding a provider or a protocol. - Forking a session is rate limited.
POST /api/sessions/:id/forkaccepts at most 10 requests per minute for one parent session and client address, and answers429 rate_limit_exceededbeyond that. list-npc-graphlists the most recently seen people first. When the cap cuts the node list, the newest nodes stay and the result says how many were left out (nodesTruncated).create-formno longer offerssubmitBehavior.immediate. Nothing read it. A plugin that still passes it is not rejected; the key is ignored. Thechar-creatorprompt no longer tells the model to send it.- A changelog entry is a file of its own. A change adds
docs/changelog.d/<slug>.mdwith its entries under### <Section>headings and no longer editsdocs/CHANGELOG.md, so two pull requests do not conflict over the changelog.pnpm changelog:previewprints the pending[Unreleased]section,pnpm changelog:release <version>moves the entries intodocs/CHANGELOG.mdat release, andpnpm checkvalidates every fragment (pnpm check:changelog). A branch opened earlier may still add lines under[Unreleased]until the next release; after itpnpm checkrefuses them. Format:docs/changelog.d/README.md. - The pre-push hook tests what the push changed. It still installs in a clean checkout and runs
pnpm checkandpnpm e2e --list, and now runs the tests of the packages the branch changed since the remote's main branch, with the packages that depend on them and the suites that read the changed files from outside their package; CI runs every suite. A change to the lockfile,turbo.json,vitest.base.ts, the rootpackage.jsonortsconfig.json, a push tomainand a pushed tag still run everything, and so doesCOVEL_PUSH_CHECK=full git pushorpnpm check:push --full. A commit that passed is remembered for 12 hours: a push that is repeated, or one that followspnpm check:push, sends at once. pnpm release:preflightfails whendocs/CHANGELOG.mdhas no section for the version inpackage.json, and the release workflow refuses to publish a tag while a changelog fragment is not assembled.submitBehavior.echoFilledNarrativeis read by the server. It comes from the committed interaction, not from the request. Withfalse, that interaction's narrative is left out of the follow-up turn's player message, as before.- On the native Gemini protocol, the turn's data and rules no longer break the prompt cache at the start of every request.
google-generative-ai-v1used to put every system message intosystemInstruction, which precedes the conversation, so the per-turn text changed the very beginning of the request. Now only the system messages ahead of the conversation go there; a later one stays in place as user text in<system-instruction>tags, as the Anthropic protocol already does, so the request is identical up to the end of the previous turn's history. Neighbouring turns of one role are merged into one, and function responses always directly follow their function call. This is the default;lateSystemAsUser = falsein the slot'sproviderOptionsrestores the earlier shape (every system message insystemInstruction) and is the way back if Google's endpoint refuses a request. The model now reads that text as user text instead of a system instruction; quality was not compared. The request shape was accepted across a tool loop by a local proxy that imitates the Gemini interface; it was not verified against Google's endpoint, and cache reads on Gemini were not measured. Seedocs/v2/reference/prompt-structure.mdanddocs/v2/reference/slots.md. - A function runtime's
ctx.gateway.generateTextcalls carry the prompt cache key. The host fillspromptCacheKeywith the hash of the session and the runtime (promptCacheKeyFor), so a runtime that keeps its instructions and stable data at the start of the request reads them from cache on the next call; a key a plugin passes is replaced. History compaction (a short fixed instruction, calls far apart), memory embeddings (no prompt cache) and world generation and translation (no session; only a retry repeats a prefix) send no key. Seedocs/v2/reference/slots.md. - Four reads no longer grow with the length of a session. An automatic snapshot takes the last ID and the compaction tags of the model history instead of reading every message with its text. A runtime that writes several characters in one turn reads the character list once for the batch instead of once per character; the name and alias check runs against the batch's own earlier writes, as before.
POST /api/sessions/:id/messages/synclooks up only the IDs in the request instead of reading every message of the session. The local media store reads and writes picture files without blocking the server, so a multi-megabyte picture attached to a story call or uploaded by one session no longer pauses the streams of the others.DataStoregainslistCompactedTurnMessageTagsandlistExistingMessageIds; a custom backend must implement both. GET /api/sessions/:id/messagesandGET /api/sessions/:id/stateare rate-limited like the paged reads: 120 requests a minute per client and path, then429withcode: "rate_limit_exceeded". Seedocs/v2/reference/api.md.- A refused
emit-eventto a topic nobody declared no longer fails the runtime. The call is still reported to the model as failed and is no business work (requireToolUseandcompleteAfterToolsare not satisfied by it), but an agent that then finishes with text orruntime-donesucceeds, so removing the plugin that consumes a topic does not break the plugin that emits it. A payload that breaks the declared schema stays a failure. Seedocs/v2/reference/tools.md. - A browser-stored session warns before it reaches the upload limit. From 80% of the 64 MiB checkpoint limit the web client shows one notice per session and page load. Before, the first sign was a refused upload. See
docs/v2/internals/storage.md. - A template variable inside a tagged block is XML-escaped, and world lore cannot close its own block. In a prompt body, a
{{ ... }}value that sits between a line starting with<name>and the matching</name>(for example{{ world.description }}inside<world-summary>) is escaped, so a closing tag in a name or a description cannot leave its block. In the<world-lore>block the kernel writes, only a</world-lore>inside the lore is rewritten; quote lines, comment markers and&in the lore read as written. soundtrackmatches a track's scene names as whole names. A name inside a longer word, or a longer scene name inside a shorter location, no longer plays the track ("Hall" is not "Great Hall"; "inn" is not in "Dunn"). The rule is the onescene-stageuses for a name inside a location.story-eventscounts turns withctx.logicalTurn, the scheduler's frozen logical turn, instead of reading the session row, so a background run counts the turn it was started for.- The session-prep world document editor shows the same over-length note as the world page when the draft is longer than the story prompt carries.
- The
codex,affinityandguideprompts use examples from more than one genre and no longer name other plugins or "working memory". - Which reasoning levels a model takes is data. The model name checks that decided it are now entries in
packages/ai-provider/src/capability/reasoning-models.data.json; the code only writes a selected level into each protocol's request. The levels offered and the fields sent are the same for every model name in the bundled tables (recorded inpackages/ai-provider/tests/__snapshots__/reasoning-characterization.snap.txt, which the weekly model-table refresh re-records), apart from the fix below. A wire a plugin registers receives the matched level and the parameter form asoptionandparameterStylein itsreasoningFieldsrequest. - The dimension tracker no longer settles a derived field. The schema and the value that
world-init/dimension-trackeris given leave derived fields out, its prompt says not to write them, andupdate-dimensionskeeps the current value of a derived field whatever the model writes.followsClockstays for rules that depend on time and are not a derivation (Emberback's power drain per hour). Pluginworld-init0.0.37. - Emberback's storm countdown is computed, not calculated by a model.
minutesRemainingis 180 minus the minutes on the station clock since the start, andstagefollows it by ranges. The prose rule that asked the tracker for the subtraction is gone. - Order no longer depends on the server's language. 66 sorts in the framework and the bundled plugins used
localeComparewithout a locale, so prompt lines, scheduling ties between runtimes, stored rows and API lists could come out in a different order on a machine with another system language. They now compare by code point. Names made of lower-case letters, digits and hyphens keep the order they had; names with upper-case letters, underscores or non-Latin characters can move. The two lists a person reads (model IDs, built-in providers) use a collator with a fixed locale. - The event trail is deleted with the traces. The
eventstable holds a copy of every event the bus publishes, the whole value of each changed plugin row included, and nothing deleted it. It now followsCOVEL_TRACE_RETENTION_DAYS(30 days by default) in the same sweeps astrace_events.docs/v2/internals/storage.md. - The character roster in the tracker's prompt has a budget. It listed every character with its whole description on every turn: 22,500 estimated tokens for a cast of 100 with a paragraph each. Within 6000 tokens nothing changes; past that every description is cut to 120 characters, and past that again descriptions are left out (1,300 tokens for the same cast). IDs, names, aliases and types are always listed.
- A history summary call has an output ceiling. The call for a summary of N tokens passes
maxOutputTokensof twice N (at least 1024), so a model that ignores the requested length stops there and not at the slot's whole output budget. A reply cut at the ceiling that already fills its budget is kept and markedtruncated; a shorter one fails the attempt as before, and the raw messages stay. npc-graphstores no adjacency index.upsert-npc-graphwrote oneindexrow per character on every change, and nothing read them: the retriever builds the adjacency from the edges. Sessions keep working;indexrows of an existing session are left in place and unused.pnpm check:i18ncompares everyt()fallback in web code with theen-UStext. A fallback that differs from the catalogue fails the check. 94 fallbacks that had drifted from the catalogue now equal it; the interface shows the same text as before.
Fixed
- A form answered in another tab or on another device shows as answered, and a lost follow-up can be sent again.
GET /api/sessions/:id/viewreturnssubmittedInteractions({ turnId, interactionId, values, followedUp }), the answers the server stored;followedUpis derived from the trace journal (a turn started after the answer). The client marks the matching form blocks answered from it on restore and reconnect whenfollowedUpis true, and replaces its own browser cache with it. A block whose answer no turn followed stays open with the stored values filled in, so one click sends it again and starts the follow-up. A submit that the server refuses as already answered shows the refusal and loads the stored answer. Before, only the browser that submitted knew the answer, so a reload elsewhere showed an answered form as open, and a lost follow-up left an answered form with nothing running. A development client that reads the view must expect the new field; nothing stored needs recreating. - A point-buy error appears under its attribute.
tabletop-rulesreturns an out-of-range attribute as{ field, message }, so the form shows it under that attribute instead of at the top; the over- or under-spent budget stays a form-level message. - PostgreSQL returns a stored string that spells JSON as that string. Plugin data, state entries and changes, event payloads, message metadata, trace payloads, job status data and runtime export values holding
"30","true","null","[1]"or"{\"a\":1}"came back parsed (the number 30, and so on) on PostgreSQL only. The PostgreSQL schema now reads ajsonbvalue once, and the shared store contract checks these strings on every backend. Stored rows are unchanged: they were written correctly and now read back as written. - A slash command typed right after a session opens is not sent to the story. The command list is read after the session opens. A command entered before it arrived matched no command and went to the narrator as a player turn, with a model call. The composer now holds the line, shows "Loading commands…" and runs it as a command when the list arrives; editing the line drops the held command.
- "Enter" on a world card does not turn into "Continue" under a click. When the session list arrived after the world list was drawn, the button a player had aimed at became the "Continue" button of the latest session, and the click resumed that session instead of opening the preparation screen. "Enter" now stays the same button and "Continue" is added before it.
- A hidden story event is given to the narrative twice.
story-eventsmarked a one-time event as fired when it handed the cue over, so a narrative that left the cue out lost the event. The turn after a cue, when no new event fires, the same event is given once more (cue.reminder: true) with the instruction to bring it in only if the previous turn did not. - A dice receipt sent after a refused one is checked. The
dice-checkguard marked the turn as accepted beforeemit-eventran, so when the event catalogue refused the receipt (a missing field), the corrected receipt went through without a comparison with the dice. The mark is now set when the event is recorded. - NPC names match as words.
npc-graphchose the graph nodes of a turn by substring, so a short Latin name matched inside other words ("Al" in "also") and a short name matched inside a longer name of another node.affinityreads full-width letters and extra spaces as the same name instead of creating a second record. world-timewrites the clock only when it changed, so a turn in which no time passes sends noplugin-data.changedevent for it.world-irsays when an item was refused because the output is full. With 32 entities an undeclared item cannot be added, and the rejection told the model to declare it; it now says that the output is at its limit and what to remove.- An embedding endpoint that never answers no longer holds a search or the vector sweep.
gateway.embed()gets the default request budget of a text call (8 HTTP requests, 120 seconds) when the caller passes none; before, it had no time limit at all. The memory layer already left retries to the gateway. - Image and speech calls show which model ran, and their tokens when the provider reports them. The
openai-imageswire reads theusageof a model billed by token (gpt-image-1); thegateway.respondedtrace of an image, speech or music call carriesusage,modelandprovider, so such an image appears in the cost panel. A model billed per image or per character reports no tokens and is listed without an amount. A plugin's own wire gets the same by returningusage. - Breaking: a role's
providerRequestMetadatacannot replace the model or the content of an image or speech request. With theopenai-imageswire,model,promptandnof the request body always come from the call, and withopenai-speech,modelandinput. Other fields of the metadata (size,quality,voice,response_format, …) still go into the body and still win over the call's options. A role that set one of the five fields in its metadata to redirect a request must name the model in the role'smodelinstead. - Generating or revising a world reports what it cost. The
doneframe ofPOST /api/ai/generate-worldandPOST /api/ai/revise-worldcarriesusage: { inputTokens, outputTokens, requests }, the sum over every model answer of the request (repeated parts, cut-off answers and the lore repair included); a failed generation carries it on itserrorframe, and the server log records it. Before, a streamed answer was recorded with zero tokens and nothing added the answers up.createWorldof@covel/createtakesonUsage. - A new voice or music model is heard at once. The stored result of
ctx.speech.generate()andctx.music.generate()is keyed by the model the role resolves to (provider, model, protocol, endpoint, wire settings) as well as the request, asctx.images.generate()already was. Before, a line already spoken kept the old model's audio after the speech role was bound to another model, until its text changed. Audio stored before this change is not reused: the first request for a line or a piece generates it once more. - A new session can no longer be stranded in setup. A submitted form with a missing or invalid value is refused with a message in the session's language that names the field, and the form keeps what the player typed. A character form the world's schema refused on every run is offered again with the earlier answers and a note; a schema default that fails its own type is left out instead of blocking. After a model failure, or when the page was closed mid-setup, the main view offers "Retry setup" or "Continue setup", which reruns only the unfinished steps. A stored point-buy allocation that breaks the rules is set aside and the form offered again.
- A suspended execution is answered with real controls. Yes/No buttons, option buttons, a text box or a form built from the schema replace the hand-written JSON, which stays as an "Advanced" path for a suspension that describes nothing.
- Loading earlier messages reports a failure, with a Retry button, instead of doing nothing.
- An execution status the client does not know is shown as "Unknown status", not as completed.
- A failed history compaction is visible. Each failure is recorded in that turn's trace with its reason (
context.compaction.failed); after three failures in a row the timeline shows a translated notice that says what to do. The plugin used to swallow every failure. - Events that fail to publish after a commit no longer vanish. Every other event of the turn is still published, the failure is traced (
commit.fanout.failed), and subscribers are told to re-read the session. - An Anthropic answer cut at the output limit is an error on a call that is not streamed. The provider's
max_tokenswas read as a normal end there, so a cut answer or cut JSON was kept as complete. Streamed calls already rejected it. - The plugin SDK's protocol type names Gemini.
PluginProviderConfig.protocoldid not listgoogle-generative-ai-v1. - The opening's facts are no longer overwritten. The opening and the first player turn share turn 1, and the
memoryplugin numbered a turn's facts from 1 both times. New facts now continue after the highest number the turn already has. - A background job reports the turn it belongs to. A detached runtime runs after its turn has committed, and it then read a turn number one too high: the
memoryplugin dated the facts of turn 1 as turn 2. A job now keeps the turn number of the execution that queued it. Facts that existing sessions already hold keep the number they were written with. - Affinity and the NPC graph count turns like every other panel. Their history stamps used the count of player messages, which includes each setup form a player submits, so a relation recorded in turn 1 could be dated turn 3. Both now use the logical turn. Stamps that existing sessions already hold are not rewritten.
- A tool result's
uiblocks are not sent back to the model. They are cards for the player's screen and repeat what the model has just written; the kernel still renders them from the full result. In play this removes about 900 characters from eachsync-codex-entriesresult. - The session page reads less after each turn. A committed state change or a finished background job re-reads only the session snapshot. Before, each one re-read the plugin list, all data of every active plugin, the world and the suspension list; with 15 active plugins and two background jobs that was about 58 requests for one turn (derived from the code, not measured), and is now about 4. A reconnect, a return to the tab and a reset still read everything.
- A failing session recovery stops retrying. It retried every 3 seconds without end. It now waits 3, 6, 12, 24 and 30 seconds, then stops and shows the events it was holding; the next state change or reconnect reads again. The events held during a recovery are limited to 500.
- Messages a player sees are translated. The turn-failure and history-restore notices, the stage error box, the summary of a failed plugin job (its raw error is in an expandable detail) and the labels of media blocks no longer show English text or a raw placeholder in the Chinese and Russian interface.
- The approval dialog says what a grant allows. Approving a community plugin's server code, or a plugin task, is explained in plain words (the code runs in the backend without a process sandbox) where the dialog showed only an internal action ID.
- The World tab and the world page show the lore of the language being played. They showed the world's own language edition in a session of another language.
- Mistport's public sections no longer state its cast's secrets. What Qi knows, where Iron Meg came from, what Lin Yuanzhou can sense and Grey Falcon's doubt moved to the narrator-only part of both editions.
- Quest change cards stay on every turn.
core-questkept the card of the latest turn only. - A failed NPC graph read is reported as failed. The retriever returned an empty success, which read as "nobody is related".
- Cancelling a turn stops a
branch-replyregeneration, which also has an output limit now.codexdoes not append a sentence an entry already holds.memoryextraction followsCOVEL_INSTRUCTION_LOCALE. - Tool calls without an id are kept. A streamed OpenAI-compatible tool call that arrived without an
idwas dropped; it now gets a position-based id, and so does a Gemini call (it had a random one, which a recorded session could not replay). Embedding results are matched to their inputs byindex, and a wrong number of vectors is an error. - A model with no known size is judged against the default context window (262,144 tokens) in every place; a preset without a profile used 64,000.
- A media file missing on disk is written again when the same bytes are stored once more (SQLite media store). A community package cannot take a bundled plugin's ID by changing letter case. The character write route validates its body and names the wrong field.
ctx.recursiveCall()no longer lets its delta change the locale or the logical turn of the nested execution. A skipped extension provider is named in the server log. A retried 408 or 409 is traced ashttp-4xx. The two full-trace endpoints share the rate limit of the paged one. - A browser-private game with a field named like a credential can be saved. The save check rejected the whole checkpoint when game content had a field such as
password,credentialsorapiKey, and the session then could not run another action. The check now reads the session record only, which is the part framework code writes. - A failed concealed runtime does not show its reason to the player. For a runtime with
io.concealed: truethe turn stream, traces and result APIs carry a fixed text (the provider and model stay); the real reason goes to the server log. - A refused
emit-eventcall is a failed tool call. An unknown topic or a rejected payload no longer counts towardrequireToolUseor a finishing tool. - Character text for the model leaves out bookkeeping.
get-character,update-characterand the character text thatmemory-searchreads no longer include session IDs, row IDs or timestamps. - The in-memory store rejects a duplicate turn-message ID, as the SQLite and PostgreSQL stores do.
GET /api/sessions/:id/suspensionsreturns only unresolved suspensions, as the reference says. The resume route computes its concealed runtimes after it has synchronised the session's plugins.- Desktop settings and key requests time out. A request to the local server ends after 15 seconds instead of waiting forever when the server stops answering.
- Token usage of a rejected structured-output reply appears in traces.
- An embedding call is not retried twice over. The memory layer no longer adds its own retries to those of the transport, and an aborted call stops at once; the
memoryplugin's extraction does not retry an abort either. - A failed world revision puts the old package back. When the new package cannot be read back or its record cannot be saved, the previous files return instead of files and record disagreeing.
- Reply Variants keeps long narrations whole. An adopted variant is no longer cut to 4000 characters. The plugin stores each turn's text once and reads only adopted turns when it assembles history.
- Bundled plugins.
inventoryandcore-questreport the changes they could not record over the per-turn limit;guideaccepts short Chinese recaps and decisions;codexmarks only the entries of the latest sync as new;char-creatorreads only its own form's submission and sets the form's ID itself; a portrait attaches to a character on an exact ornpc-<id>match only;list-npc-graphreturns current relationships, newest first. - Plugin data of a session you left is released in the web client.
- The context budget protects the player's message. A
user-role section a plugin places after the history no longer takes the protected place of the player's input. - Dimension settlement receipts are pruned. The 20 newest resolved receipts stay; unresolved ones are never removed.
- The API quick start runs as written. Its action requests no longer carry
locale, and its last step says that it deletes the session. - An agent's output is checked against its
output.schemaagain after aPostRuntimehook rewrites it, as a function runtime's already was. A rewrite that breaks the schema fails the runtime withoutput-schema-invalidand commits nothing. - Mistport's faction relations no longer state Iron Meg's past as a Guild apprentice, which the world keeps in its narrator-only section.
- A countdown or deadline dimension now follows the world clock. The dimension tracker (
world-init/dimension-tracker) did not receive the world time, so a rule such as "minutes remaining is 09:14 minus the station clock" stayed at its start value on every turn in which the story text named no time, while the clock next to it moved. The tracker now reads the clock as settled for the current turn, andupdate-dimensionstakes afollowsClocklist: in a turn in which the clock moved, a settlement that leaves out a dimension named there is refused with the clock in the message, so a turn in which nothing else happened no longer leaves the countdown where it was. The bundled world Emberback states its storm countdown as a function of that clock. - Answering a form stores the answer and starts its turn as one operation. They used to be two requests, so a second tab, a retry or a page closed between them could run the follow-up turn twice or leave an answer stored with no turn. The answer is now checked under the session lock and written in the same transaction as the turn's start; of two requests that submit one form together, one runs the turn and the other is told the form was already answered. A turn that has started runs to its commit when the connection drops.
- A player name that a world character has as an alias no longer blocks setup. Bundled worlds declare first names as aliases (
Tomas,June,Mina); a player who chose one had the character refused at commit on every setup attempt, with the form already answered, until setup was blocked.char-creatornow registers the form validatorplayer-name: the name is refused when the form is submitted, with a sentence under the field in the session's language, nothing is stored and the player submits the same form again. The form carries salted digests of the taken names, never the names: the form block goes to the client.char-creatoris now 0.0.36; an existing session runs its setup step once more, which does nothing when the player exists. If such a name is already stored (the world's characters changed, or the form was made earlier), the setup guard offers the form again with a note instead of proposing a character the World Model refuses. Rule:docs/v2/reference/world-model.md. - A failed last turn keeps its retry after the trace retention period. The execution status and the retry of a failed or interrupted turn are read from that turn's
turn.startedtrace row, and the retention sweep deleted it with the other old traces: a session opened again after 30 days showed no retry. The sweep now keeps the traces of each session's newest turn. - An agent that writes too large a plugin-data value is told so.
plugin-data-setandplugin-data-set-batchnow fail at the call with the value's size and the 256 KiB limit ("store less, or split it across several keys"). Before, the tool reported success and the value was refused at commit, where the runtime's other writes of the turn were dropped with it. The limit and its helpers (MAX_PLUGIN_DATA_VALUE_BYTES,pluginDataSizeBytes) are exported from@covel/shared;@covel/runtimeno longer exports them. - A story call with pictures can fall back to the slot's next model. When the first model of a slot failed and the request carried pictures the player had seen, the gateway sized the request for the fallback model with the base64 image bodies counted as text, found that it "cannot fit", and never tried the fallback. An image now counts as 1,600 tokens in that check. A fallback model receives the pictures only when it is known to accept image input; otherwise it gets the text of the request, the picture notes included. See
docs/v2/reference/protocol.md. - A form answered late in a tool loop reaches the model where it was asked. An endpoint that returns no tool-call ID (Gemini models often, some OpenAI-compatible relays) got
call_<position>, so every step of a loop had acall_0, and resuming a runtime that suspended at a later step overwrote an earlier step's tool result with the player's answer. A missing ID is now a digest of the request and the call's position: different in every step and every runtime, and the same when a recorded session is replayed. Resume replaces the last result under the suspended ID. - Gemini: function responses are a content of their own. Text that follows tool results (a player message, a correction hint, a late system instruction) is the next
usercontent, as it was before late system messages stayed in place and as the AI SDK's Google provider sends it. Only plain text neighbours merge. Neither shape has been checked against Google's endpoint;lateSystemAsUser = falsestill restores the earlier request. - A 400 from a Responses endpoint is read with a size limit. The check for a model that refuses encrypted reasoning read the whole error body without one.
- A form answer is held to the length of a player's message (100,000 characters). A longer text field is refused under its own field, and fields that make a longer message together are refused for the form as a whole. Before, the turn after such an answer could fail and its retry was refused.
- A form validator that throws no longer shows its error text to the player. The submission fails with
form_validator_failedand a fixed text; the plugin's error is in the server log. - A session in a world's other language edition now gets that edition's lore. The session-prep screen sent the world's default-language text as a lore override on every start, so a session created in another content locale still had the default edition in its prompts. The prep editor now shows the edition of the language the session will play in, and an override is sent only when the player changed the text.
- Scene cast no longer picks characters named inside other words or longer titles.
scene-stageread a character as present when any alias appeared as plain text, so "Rin" matched "spring" and "President" matched "Vice President Shiraishi". Cast selection andnpc-graphretrieval now sharementionedCharacterIdsfrom@covel/plugin-handlers-utils: names of spaced scripts need word boundaries and a name inside a longer name of another character does not count. Reference:docs/v2/reference/plugins.md. - Mistport character cards no longer show the secrets the world text keeps from the player. The character fields and the preset-character cards now carry the same public hints as the world text; the hidden facts moved into each character's narrator-only
persona.secrets, in both language editions. - Installing or updating a world in Settings refreshes the world list. The home screen and the session-prep screen showed the old list or an old world record until the page was reloaded.
- A world record invalidated while it was being fetched is read again. The reader no longer stays on "loading" and the session-prep screen no longer keeps Start disabled.
- A failed first read of the server's settings is retried. The settings store now retries three times with growing waits (1, 3 and 9 seconds) instead of only when the window regains focus.
- A package update that cannot be recovered no longer stops the server from starting. The server logs the package and the reason, writes the reason where the install pages show it, keeps the update's directories untouched for the next start, and continues without applying it.
- The memory plugin no longer retries a failed model call on top of the gateway. During an outage one extraction could make up to 24 requests; it now makes the gateway's own attempts only.
- A second server process on the same SQLite file is refused at start. The server keeps
<SQLITE_PATH>.lock(pid and start time) next to the database; a second process, for example a dev server started against the desktop app's Covel home, fails with a message naming the file and the owner's pid. A lock left by a killed process is taken over. Seedocs/v2/internals/storage.md. - Emberback's Chinese edition calls the crawler one thing. The world data used three different words for it, and the road-travel rule triggers on only one, so narration with another word could miss the rule; all files now use that one word.
- Emberback has one clock. A station time definition (
data/time.yaml, starting 06:14) is added, and the Crownfire countdown and its rule now say the storm arrives at 09:14 station time and the countdown is that clock expressed as time left. - Mistport's English edition matches the original. Facts the Chinese text does not state (assassination attempts, hidden caves, a tripled congregation, relic examples and more) are removed, and a missing detail about the Guild's apprentices is restored.
- The schema reference no longer offers inline locale maps. The field tables said "string or locale map" for text fields although
validate:pluginandvalidate:worldreject an inline map; they now say "text" and point to locale files. - World-model validation no longer slows down with the number of characters. Name and alias conflicts are checked against one table of name keys instead of comparing every character with every other, and
ctx.worldvalidates only the writes buffered since its last read. With 200 characters of 3 aliases each and 15 buffered writes, one read ofctx.world.characterstook 2.6 s and now takes under 10 ms. The rules are unchanged. - Answering a form reads only that turn's messages. A submit read the session's whole message log three times.
- A background job queued before a browser-private session was restored reads the runtime exports it should. The checkpoint kept only the newest revision of each export, which a job frozen at an earlier instant cannot read; it found none and was skipped. While a job is unfinished the checkpoint now keeps the revision that was live when the job's source execution began and the later ones.
- A tool call without an ID is kept in a Chat Completions reply that is not streamed. It was dropped, so the model's call never ran; it now gets the same derived ID as in a streamed reply.
- Bundled world translation files hold only translated text. The
.en-USfiles oflantern-barrowandmistportno longer copy structure (insertion order, enabled flags, conditions, attributes, tags) from the main files; changing the main file no longer leaves a stale copy behind. The merged result is identical. role:*preference tags are gone from the bundled worlds' plugin policies, since no plugin declares them.- A Qwen model named with capital letters sends the level it was offered. The offered levels were read from the lower-cased model ID and the request fields from the ID as written, so
Qwen/Qwen3.8-27B,Qwen3.8-Max,Qwen3.8-FlashandQwen3.8-2.4T-A95Bon an aggregator sentenable_thinkingwithoutreasoning_effortforlow/medium/xhigh, and the thinking-onlyQwen3-…-Thinkingmodels sent nothing for a level they do not offer where the lower-case name keeps thinking on. Thirteen names in the bundled table were affected. - A world deleted while the app loads no longer stops it from starting. On the first visit with browser storage the app reads every world of the catalog; when one was deleted between the list and its read (another tab, another player of a shared server), the app showed "Startup failed: World not found". That world is now left out.
- A resume that fails no longer reopens a suspension another resume has taken. When a resume ran longer than the stale-claim limit (one hour), a second resume could take the suspension; the first one, on failing, then cleared the second one's claim or its recorded result, and the same suspension could be resumed twice. A resume now releases only its own claim.
- A background job stops when it can no longer renew its lease. A renewal that failed was only logged, and the job ran on after its lease had expired, while maintenance could already have marked it
orphaned. The job is now aborted once the lease has run out unrenewed and ends asorphanedwith the reasonlease-expired. Reference:docs/v2/reference/protocol.md. - A load-time input check no longer calls a binding compatible when it cannot tell. When
acceptsconstrains a property that the producer's schema does not declare, the producer may send any value there. The check now reports this as undecidable (a warning; the value is still validated at run time) unless the producer forbids undeclared properties. - A model that accepts a request and then sends nothing no longer uses up an agent's whole time. The first-token wait was 120 seconds, the same as the
timeoutMsof the bookkeeping agents, so the retry thatmaxRetriesallows was reported at the moment the runtime timed out and never ran. The wait is now bounded as described above, so a stream that stalls once is repeated and the runtime succeeds. - A text call of a function runtime that gets no answer is sent again.
ctx.gateway.generateText,generateObjectandevaluateare not streamed, so a request the provider accepted and never answered held the handler untilfunction.timeoutMs(or the limit of its detached job) ended it. The host now gives up such a call after half of the time the runtime has left, at least 60 seconds, and sends it once more. A runtime with 60 seconds or less makes one attempt as before. Rule:docs/v2/reference/plugins.md. - A failed extraction no longer stops the next turn. When the shared extraction of a turn failed, the dimension settlement of that turn was held as pending even though the tracker had settled it from the narrative, and every later turn was refused until the player pressed Retry. The tracker's settlement now stands.
- A pending dimension settlement is retried before the next turn. The next player action first runs the recovery that the Retry button of the pending-settlement notice runs. The turn is refused only when that fails too, and the message now says what to press. Rule:
docs/v2/reference/dynamic-dimensions.md. - Memory asks for its update as JSON, and a reply it cannot read is on record.
memory/extractpasses the shape of its update asresponseFormat, and writes the start and the end of an unreadable reply to the log (the trace keeps no reply text). - A reloaded session no longer stays on "Restoring the save". When the saved view arrived at the same moment as the answer of the execution status check, the check put the session back into its restoring state and repeated that every three seconds, so the stage or the chat never appeared until the player reloaded again.
- Three Chinese characters are readable in the Stage style's titles. The bundled display font draws U+56DE, U+5703 and U+5704 as solid squares; titles now take them from the next font in the list.
Security
- Plugin HTML runs under a policy of its own, and the app's page allows no inline script. A plugin
webviewwas asrcdocframe, which inherits the policy of the page that creates it, so the app's page had to allow inline scripts for every plugin panel. The outer frame is now/plugin-frame.html, a static document of the build loaded by URL into a sandboxed opaque origin with its own policy (inline script and style, no network, no navigation); the server also sendssandbox allow-scripts; frame-ancestors 'self'for it. The plugin's HTML reaches it over the bridge port, not through a URL, so the address names no plugin, session or file. The bridge parses every message from plugin HTML with Zod and hands out no token. The app no longer triggers a policy report on every load (Zod probed foreval).tests/e2e/page-policy.spec.tsfails on any policy violation on the main screens and in a session with plugin panels. Details:docs/v2/internals/security.md. - Hosted tiers have no loopback exception. With
DEPLOYMENT_TIER=demoorcommercial, a model or plugin request tolocalhost,127.0.0.1or::1is rejected by both the URL check and the connection check; a player could name such an address in a request-scoped model preset. The exception for local model servers applies on theselftier only. - A plugin's text protocol serves only the sessions that enabled the plugin. A model whose
protocolnames a plugin's wire fails with a configuration error that names the plugin when the session does not have it active. A request with no session may use a builtin plugin's protocol, and on theselftier a community plugin's too. - Snapshot responses leave out hidden content.
GETandPOST /api/sessions/:id/snapshotsno longer return hidden world data, the exports of concealed runtimes or the prompts of suspensions. The stored snapshot is unchanged, so restore and fork work as before. - Provider responses are read under a size ceiling. 256 MiB for a JSON or text body and for one streamed event, 50 MiB for audio bytes. A stream has no total limit.
- The web client sends only the provider keys its requests can reach.
X-Provider-Keyscarries the providers that the model roles resolve to (saved binding, otherwise the server's provider) and any provider a request names directly, instead of every saved key. Until the server's roles and presets are loaded it still sends all keys. Seedocs/v2/internals/security.md. - Rate limits cannot be evaded by rotating path parameters, and their table is bounded. Besides the budget of each path, a client address has one budget per route template, eight times as large, and the limiter keeps at most 10,000 counters, evicting the oldest.
NODE_ENV=testno longer moves outbound requests off the guarded transport. With that variable set, provider requests went through the process's globalfetch, not the npm Undici client that the DNS-pinning and proxy dispatchers belong to. The transport now uses the globalfetchonly where a test run's own setup code has set a mark in the process.docs/v2/internals/security.md.- Image uploads are checked by their bytes.
POST /api/medianow requires the body to start with a PNG, JPEG, GIF, WebP, AVIF or BMP header and stores the type the bytes declare, so a file labelledimage/pngthat is really markup or script is refused with 400.
Documentation
docs/v2/reference/ui-panels.mddescribes the__turnIdconvention of message blocks; the theme pages state that an imported theme's CSS can load remote resources.world-timeexplains why a random time step derives from the turn instead ofctx.random.chromium-devPlaywright specs wait up to 20 seconds for an assertion, since the dev server transforms modules on first load.- The plugin author guides match the current contract. Forms (per-field errors,
notice, answered once), hooks (ctx.pluginId/ctx.runtimeId/ctx.locale), thectx.pluginDataread shape, the 256 KiB value limit,ctx.random,agent.looplimits, event-topic ownership, concealed runtimes and text wires are now indocs/guide/plugin-authoring*.md,plugin-ui-runtime-guidelines.mdand thecreate-pluginskill.docs/DOCS_STRATEGY.mdsays which pages have a language sibling. docs/v2/reference/protocol.mdandapi.mdlist only events that are emitted.docs/v2/reference/world-data.mdsays thatmedia/portraits.jsonandmedia/scenes.jsonare read only by the art generation scripts.- The flow page points to
docs/v2/internals/packages.mdfor package responsibilities instead of repeating them. - Guides use
contributes.toolsandpnpm e2e:verifywhere they namedtools.pluginandnpx tsx scripts/e2e-plugin-verify.ts. - The 0.0.19 section of the changelog has its heading and link again, with the missing links of 0.0.32 to 0.0.35.
- The security page states that
resolveSlothands a plugin any slot's key.ctx.gateway.resolveSlot()returns credentials for any slot name by design (custom wires need them) and plugin server code is not sandboxed, so approving a community package already means trusting its author with the provider keys; seedocs/v2/internals/security.md.
Upgrade notes for v0.0.50
- Clients that read a refused
submit-form: match oncode: "form_rejected"; the message text changed and is now translated. - A
history.compact@2provider reports a failure by throwing. Returningnullmeans "nothing to compact"; a provider that returnsnullon failure still works but its failures stay invisible. - A commit refused for a dimension version conflict carries
code: "dimension-version-conflict"; the message no longer starts with that prefix. A pending settlement receipt written before this version with a failed extraction is no longer treated as blocked and settles on the next attempt. - Hosted operators with a model server on the same host: an
llm.tomlendpoint on a loopback address is rejected ondemo/commercial. Use a publichttpsname. - A plugin that registers a text protocol declares it. List the wire's
idincontributes.wires, as for image and speech wires; an undeclared text wire now fails the load. - Reply Variants data of existing development sessions is not carried over. Earlier turns keep their blocks; an earlier adoption no longer rewrites history until the variant is adopted again.
- Bundled plugin versions changed.
codex,affinity,guide,soundtrackandstory-eventshave new versions; setup completion is recorded per plugin version, so a development session recreated or continued on the old versions re-runs setup guards (they skip finished work).
[0.0.49] - 2026-10-09
This release fixes session recovery, background-task status, streaming retries, plugin and storage boundaries, and desktop layout issues. SQLite now uses Node's built-in driver while retaining the existing database format. macOS Apple Silicon and Windows x64 binaries are unsigned; the macOS build is not notarized. Read the upgrade notes before installing.
Added
- Recorded scripted sessions replay in one command, on any machine.
pnpm e2e:replayplays each session undertests/llm-replay/against its recording: it starts the replay proxy, a test server with a new database, its own home directory, the session's random seed and UTC, thenscripts/e2e-plugin-verify.ts, and stops them. The server reads nothing from.env,~/.covelor the shell'sCOVEL_*variables, so another checkout sends the same requests; any request missing from the recording fails the run. Recordings are committed, one answer per request named after its digest.--record --upstream <origin>records a session again: answers already recorded are reused, only new requests reach the model, and answers the session no longer asks for are removed.pnpm llm:replaystays for ad-hoc recordings underdebugs/. Thelantern-barrowsession is defined but not recorded yet: record it withpnpm e2e:replay --record --upstream https://api.deepseek.com lantern-barrow. Verified with a fake model on a session that stopped at character creation (8 requests, all answered in a copy of the repository at another path, with anotherHOME, time zone andLANG); not yet verified on a recorded three-turn session. Seedocs/v2/ship/real-model.md.
Changed
- SQLite runs on Node's built-in
node:sqlite; better-sqlite3 is gone. The native package needed node-gyp or a matching prebuild on every install, and an ABI match for the desktop sidecar.SqliteStore, the SQLite media store and the sqlite-vec capability now useDatabaseSyncthrough a small Drizzle driver inpackages/store/src/sqlite/drizzle-node-sqlite.ts, on Node 26 and on the Node 24 inside Electron 44. Existingcovel.dbfiles open unchanged. The unusedpgpeer that drizzle had resolved left the lockfile with it. - The Playwright suite runs against a production build.
pnpm e2ebuilds the web app intodist/web-e2eand serves it withvite previewnext to the Vite dev server. A fresh browser context fetched the dev server's several hundred modules on every page, all from one Vite process, so more workers did not make the suite faster. The 23 tests whose specs import the app's own modules in the page (import("/src/...")) still run on the dev server, in thechromium-devproject; the configuration finds them by reading the specs. On one 4-core machine the whole suite went from 6 min 5 s to 4 min 26 s with the default workers, and to about 4 min with 4. The build is made on every run, so a change to the front end needs no manual build. Seedocs/v2/internals/e2e-testing.md. - A plugin outside the host range it declares is no longer loaded.
covel: ">=0.0.45 <0.1.0"inPLUGIN.mdwas checked only when a package was installed, so a package copied into the plugin directory, or installed before the app was updated, still ran on a host it does not support. Startup and reload now apply the same check: the package is listed as failed with both version numbers, and none of its code runs. A package withoutcovelloads as before. pnpm create-pluginwrites the host range of a standalone plugin. The generatedPLUGIN.mdcarriescovel: ">=<version>"with the version of the checkout that made it, so a host older than the one the plugin was written on refuses the package with both version numbers.--with-tools, which scaffolds a bundled plugin underplugins/, writes no range.- Inside the app, the interface language is in Settings only. The top bar of the session and debug pages no longer has a language select; Settings → General → Interface Language is the same setting. The landing page, which has no way into Settings, keeps the select, and so does the menu of a narrow window.
- The narrators' character sheets show a number attribute with its range.
characterSheetSegmentstakesschema(passctx.world.characterSchema); a number attribute whose schema declares min/max renders as"might": "2/5"in the player sheet and the NPC profiles, so the model can tell a maxed strongman from a feeble 5/100.narratorandchat-mode-narratorpass it. - The dice-check roller pre-computes the modifier of each ranged number attribute. Without a conversion rule the narrative used the raw value as the modifier (a 2/5 became +2), which made checks fail far more often than the DC table intends. The injected
checkContextnow lists each of the player character's ranged number attributes with its converted modifier ((value - min) ÷ range × 10: a 2/5 is +4), and the narrative reads the modifier from the list instead of estimating it. Attributes without a declared range are still converted by the narrative.
Fixed
- A filled opening form no longer repeats a full stop supplied by both the field and its template. Narrative interpolation omits a template
.or。immediately after a value already ending with that character. Stored player input and punctuation within the value remain unchanged. - Completed background tasks no longer accumulate as failed turns below the story. Restoring a worker's model reasoning created a separate foreground task that was then marked interrupted, even when its durable job had committed successfully. The timeline now joins worker traces to the job's source turn using
backgroundTurnId, preserves the durable outcome, and deduplicates reasoning across refreshes and either hydration order. Actual job failures remain visible under their source task. - A suspension can be resumed again after the server stopped in the middle of resuming it. The claim a resume takes was never released when the process ended first, so every later resume answered 409 and the player could only abandon it. The claim is now released at the next server start (SQLite) or after one hour (PostgreSQL).
DataStoregainsreleaseStaleSuspensionClaims. - A settled tabletop check reaches the narrative without row IDs. The check text the narrators read carried the form submission ID and the turn ID of the stored receipt. It now carries the check only: action, attribute, modifier, die, difficulty, total and outcome.
- A media download drops a redirect's body unread.
ctx.media.ingestUrlread the whole body of each 3xx response, whichmaxBytesdid not limit. - The model database refresh times out while the body downloads. The 30-second limit ended when the response headers arrived, so a stalled body kept the refresh running.
- A detached runtime's proposal is checked again after a
PreStateCommithook replaces its payload. The declared-effects check ran only before the hook, so a hook could move the write to a namespace the runtime did not declare. - Operator Access says when the browser refused to save or clear the token. The pane showed the change as done and reloaded the app.
- The dice-check roller resolves a translated attribute name like the rest of the app. An attribute name with no entry for the session locale fell back to the first entry instead of the same language or English.
pregamereads the world throughctx.world. It called a store method that only bundled plugins receive.- World text. Emberback's Chinese opening no longer has an untranslated word with a wrong gloss; Chihiro Onodera's English character card uses "she" throughout.
- A concealed runtime's output is no longer readable through the runtime-outputs endpoints.
GET /api/sessions/:id/runtime-outputsand/runtime-outputs/:outputIdreturned the stored row of a runtime that declaresio.concealed, with its output text and tool arguments and results, while/turns, traces and the live stream hid them. Both endpoints now return such a row with emptyresultsand withinput/outputof each tool call set tonull. - The plugin-data
_indexendpoint no longer lists hidden world data. It returned the namespace names and keys of_hidden.*data and of kernel bookkeeping, which every other plugin-data read leaves out. - Desktop: a plain-HTTP link to a host whose name starts with
127.asks for confirmation.http://127.0.0.1.example.com/was treated as a loopback address and opened in the system browser without the plain-HTTP dialog. Only127.0.0.1,localhostand[::1]open directly now. - A Latin lorebook key matches when it touches Chinese, Japanese or Korean text. The whole-word rule counted a character of those scripts as a letter, so the key
Alicedid not match "我去找Alice谈谈" and the entry was left out of the prompt. A character of those scripts now ends a word;artis still not found instart. - Keyword recall finds a Latin name written next to Chinese text. "Alice去了北方的灯塔" was indexed as one term, so a search for
Aliceor灯塔did not match the message by term, and the substring fallback compared a lowercased query with the original text. Latin words and runs of Chinese, Japanese or Korean are now separate terms, and the fallback ignores case. The archival search uses the same terms. - A retried model call streams its text. Only the first attempt of a streamed call sent its text on, so after a first attempt that failed before any output the narration of the retry appeared all at once when it ended. A retry now streams unless an earlier attempt already sent text.
- A stream that breaks while the model is still reasoning is retried. Reasoning is never shown to the player, but it counted as output the player had read, so a connection lost during a long thinking phase failed the turn instead of repeating the call. Only text that was shown ends the call now.
- A background job that ends right after an action no longer stays "running" in the page. The event stream pauses during an action and resumes from the last event ID it received. A stream that had received no event yet (a new session, or right after a reset) had no ID to resume from, so the server replayed nothing and an event emitted in between was lost: the step stayed pending, and a session kept in the browser did not save that result.
system.connectedandsystem.resetnow carrycursor, the ID of the newest event, and the client resumes from it. - Align Memory, SQLite and PostgreSQL store contracts: reject duplicate ordinary message inserts and repeated checkpoint message IDs before replacement; refresh snapshot capture timestamps; and index SQLite turn-result append positions by session, timestamp and sequence. MemoryStore checks message identity through a global index that follows rollback, savepoints and session deletion.
- A client that stops reading its action stream no longer holds the session lock. Action and subscription SSE writes go through one bounded queue per connection (256 frames, 2 seconds per write and for the closing drain); execution and commit callbacks only enqueue. An overflow, a write error or a missed deadline closes the connection without changing the committed result, and the client recovers through the read-only execution and session endpoints.
- A provider error that refuses the request is not sent a second time. When a stream failed before any output, the agent loop repeated the call without streaming, also after a 401 or 400 that fails the same way again. The repeat is kept for failures a plain call can get past.
- A failed model response keeps its token usage in the trace. A response cut at the output limit, or rejected after it arrived, was recorded with zero tokens, so the Debug cost panel undercounted retries.
- A model that streams a long tool call is not timed out while it writes. Fragments of a tool call's arguments count as output for the first-output and idle limits on the OpenAI Chat, Responses, Anthropic and Gemini wires. The call is still released only when it is whole, and a shown stream that breaks before any text is retried.
- A session kept in the browser starts with its world's character schema. A session created on the server takes
metadata.characterSchemaof its world as its schema; one created in the browser-private profile started with none, so the world's preset characters had fields without a schema. The browser now writes the same schema and refuses an invalid preset before it saves the session. - MemoryStore keeps a row's identity on update, as SQLite and PostgreSQL do. Updating a state entry gave it the ID of the new record, and updating a world, a lorebook entry or a suspension replaced its creation time (for a suspension also its turn and runtime). The browser-private profile and tests run on MemoryStore, so they saw IDs and times the SQL backends never produce.
- The scene stage reads its cast through the public plugin API. Its cast and direction runtimes listed characters with a store method that only bundled plugins receive, so the same package installed as a community plugin chose speakers from an empty roster. They now read
ctx.world.characters, and no bundled plugin refers to an internal package type. - A media write that outlives its runtime is no longer claimed for the session.
ctx.media.putandctx.media.ingestUrlrecorded ownership and a reference after the bytes were stored, also when the runtime had timed out or been aborted while they were written. The call now rejects and the bytes stay unowned for media cleanup. - The browser's execution-timeline cache stops growing. Each save merged into every row ever saved for the session and rewrote the whole record. The cache now keeps the newest 1000 runtime executions of a session.
- The request budget counts a response schema twice. The schema is written into the system prompt and, on the Responses and Gemini wires, sent again as a native field; the budget counted one copy.
- World ZIP activation rejects worldData errors with 400 before creating a record and removes only the new directory, allowing a repaired same-ID retry; warnings and startup seed tolerance remain unchanged.
- Active EventBus subscriptions preserve replay epochs and sequences beyond the pinned-state eviction budget; released sessions remain eligible for normal LRU/TTL eviction.
- Character sync overlays only new batch proposals onto world snapshots, preserving same-batch deduplication and sequential updates. Missing-character updates fail as tools, preventing partial function-runtime commits and false agent completion. Function tool calls preserve the execution's frozen logicalTurn.
- Unexpected resume, snapshot and fork errors use standard production sanitization after cleanup; resume lock timeouts return
503 session_busy. A resume whose runtime or commit failed still says why, as a turn does, without returning the runtime result. World translation streams sanitize unexpected errors without changing their open HTTP status, and still report why a translation was refused. - A revised world stays in its own language. A revision removes the other editions of a world and writes no locale files, but the manifest the model returned could declare more editions (a request such as "add an English edition" does this), and a session in that language then got the original text. The revised manifest is now set back to the world's own edition.
- Request-body limits return standard 413 envelopes, not 500, for declared-length and streaming requests at the existing 1/20/64 MiB limits.
- Development reload recomputes plugin layout diagnostics so repaired quarantined community packages recover static declarations without importing unapproved entry, handler or guard modules.
- Plugin CLI/ZIP validation checks committed input schema files and same-package producers and rejects committed+select before installation writes; normal turn select and optional external contracts remain supported.
- Theme application, import and save-as await persistence without clearing overrides or names or reporting false success on failure. Failed appearance edits, resets and ignored token submissions restore saved previews while protecting later edits and scheme switches.
- Plugin catalog switches have accessible names and native keyboard activation without submitting forms; inputs, textareas and selects associate localized labels with stable unique IDs while retaining the panel interaction lock.
- English and Russian model-role connectivity hints direct players to the current Providers & Models page instead of the retired API Keys pane.
- Disabled Lorebook entries are excluded from archival keyword/vector retrieval as well as automatic prompt injection, including existing vectors before ingestion runs. Re-enabling restores search; ingestion removes disabled indexes and rebuilds them when enabled again without affecting other owners or characters.
- Desktop key saving while the sidecar is unavailable now writes a private temporary file and atomically replaces the saved keys; write, permission-setting and rename failures preserve the existing keys.
- Execution status reads reject a session deleted or recreated during an authorized lookup, rather than returning the replacement session’s retry input; polling remains non-blocking.
- Newly generated worlds declare only the requested content-language edition. Generation and revision validate the complete character roster against the world’s character schema before accepting it, preserving supplemental retry/discard and whole-revision failure behavior. Existing revision language declarations are not clamped.
- Read-only session recovery replaces terminal streaming fragments with durable narrative, bridges non-overlapping message windows without dropping loaded history, and prevents stale plugin catalogue reads from overwriting a newer visit or observation. Healthy POST streams remain authoritative.
- A session reloaded in Stage keeps its world, its view and its decision. After a reload the session could be restored before the world list had loaded, and it then had no world: no world name or art, and a world whose default view is Stage opened as text. The session now takes its world from the list when the list arrives. The view a world asks for also applies when the world arrives after the page, until the player picks a view. And a story that the restore delivers after Stage is on screen is no longer replayed in the dialog with the decision hidden until the player clicks through it: the restore counts as running a turn until it has checked the session, and a story that arrives then is history. Each depended on the page loading before the restore finished, which the production build does (the desktop app, a served build) and the dev server rarely did. Found by running the browser tests against the production build.
- The Windows window buttons no longer cover the right end of the top bar. The desktop shell draws minimize, maximize and close over the top-right corner of the page, and the top bar reserved no room for them, so the control at its right end (the language select) was hidden. The top bar now leaves that width free. A layout with the icon rail has no top bar, so the first row of each page was under the buttons; on Windows it now gets a strip of the buttons' height above the page.
- The Windows window buttons take the colours of the page under them. They were drawn on near-black with light symbols in every theme, a dark box in the corner of a light one. The app now reports the background and text colour of the top bar (of the page, in a layout with the icon rail) when the theme or the colour scheme changes, and the shell draws the buttons in them. Over the startup screen the buttons follow the system colour scheme, as that screen does.
- The schemas of
story-eventsandscene-stageno longer make Ajv logstrict modewarnings in the server log and in test output. They accept the same values as before. - A corrected agent is required to call a tool. A runtime that must use a tool (
completion.require: tool-use, or a completing tool) and answered in prose got one correction, but the next request still left the choice to the model, which answered in prose again and failed. The corrected request now sendstoolChoice: "required"(a tool the manifest names stays the choice; a thinking model keeps automatic choice), and the choice is the model's again once it calls a tool. - A rejected tool call names the fields in its error line.
Invalid parameters for tool …: Invalid input: expected array, received undefineddid not say which field; the line now readsfields: Invalid input: …, as thedetailsalready did, and a model fixes the call on its next step instead of repeating it. - Reasoning a model writes inline as
<think>…</think>no longer reaches the reply. Qwen3, DeepSeek-R1 and their distills reason in the reply itself, and a gateway may pass that through as content; a story then showed the reasoning to the player and a JSON reply did not parse. The Chat and Responses adapters move those blocks to the reasoning channel, streamed replies included, and do not send them back on the next request. - A bookkeeping agent retries a model call that broke off mid-stream or ran into the output limit. Every agent runtime streams, and a stream that failed after its first output was never retried, so
maxRetries: 3onguide,affinityand the other bookkeeping agents covered only failures before any output. Only a story runtime shows its stream to the player; every other runtime now drops the partial output and retries. A response cut at the output limit is retried once: a model that looped, or one that echoed its tool call into the reply as escaped text (grok-4.7-build-fastbehind some gateways), usually finishes on a fresh attempt, and a second cut fails with the message to raise the limit. The time a failed stream spent writing is credited back, as a finished stream's already was, so the retry still has time to run. - The memory extraction of a turn is no longer lost when the player answers quickly. A background job's
maxExecutionMsalso counted the time its finished result waited for the session lock to commit. When a job outlastedmaxSettleWaitMs, the next player turn went ahead and held that lock for its whole run, so amemory/extractthat finished in 90 seconds was timed out at 120 and not retried. The limit now ends when the runtime returns; a job that was cancelled or timed out is still refused at commit. The log no longer warnscompletion follow-up failedfor a job that timed out. pnpm e2e:verifyruns without.envand.env.llm. It required both files although the harness reads no provider key (the server does), so it could not start where the keys are environment variables. It now loads each file only when present, likepnpm i18n. The plugin-testing guide's example passed--plugins, which the harness rejects; it now passes--plugin.- A missing user worlds directory takes one line in the server log. On a fresh install
$COVEL_HOME/worldsdoes not exist until the first world is installed, and the seeder and the file watcher each printed anENOENTstack trace for it. The directory still counts as an unscanned source, so stale worlds are still kept until a complete scan.
Upgrade notes for v0.0.49
- Upgrading from v0.0.48 does not require deleting SQLite data. Existing
covel.dbfiles remain readable after the switch tonode:sqlite; this release adds no data migration. Keep a backup before upgrading. Upgrades from versions before v0.0.48 still need the v0.0.48 breaking-change notes, and versions before v0.0.42 also need the v0.0.42 guide. - Custom storage backends must implement
DataStore.releaseStaleSuspensionClaims(olderThanIso): Promise<number>. Built-in backends already release abandoned resume claims according to their startup and expiry policy. - Plugin host ranges are enforced at startup and reload. A plugin whose declared
covelrange excludes0.0.49will not run; authors should test the package and update its range when supported. Plugins without a range keep their existing behavior. - Desktop downloads are unsigned. macOS builds are also not notarized, so the operating system may require first-launch confirmation. There is no signing or notarization claim for these artifacts.
[0.0.48] - 2026-10-07
This release lays out the prompt so that a provider's prefix cache reaches the history, keeps IDs and timestamps out of what a model reads, settles more of the slips for which a tool refused a model's call, adds sessions that replay from recorded model calls, and fixes the findings of a whole-codebase audit. It does not read the data of an earlier version: read the upgrade notes before installing it.
Added
- A scripted session can be recorded once and replayed without model calls. A model call is in flight for nearly all of a session's wall time, and two runs of one scripted session did not send the same requests: of 44 requests, at most 1 matched. Three things make them match now.
pnpm e2e:verify --session-id <id>creates the session under a fixed ID. A test server started withCOVEL_RANDOM_SEEDgives plugins the same dice and the same random part of an ID on every run. And lists of plugin data reach the prompt in the same order (see Changed).pnpm llm:replayis a proxy in front of an OpenAI-compatible endpoint, built on@copilotkit/aimock:--mode recordsaves each answer,--mode replayanswers from the recordings, andpnpm llm:replay:diffshows where two runs differ. A three-turn session oflantern-barrowwith 47 model calls took 229 seconds to record and 4 seconds to replay, all 47 from the recordings. Verified on that session only. Seedocs/v2/ship/real-model.md. - Plugins draw dice from
ctx.random. Function handlers, agent guards, plugin tools and RPC actions getctx.random.int(min, max), with the contract ofrandomIntofnode:crypto, which it is unless the server has a seed. WithCOVEL_RANDOM_SEED, the n-th number a runtime (or an RPC action) draws in a session is a function of the seed, the plugin, the runtime and n.dice-checkandtabletop-rulesuse it.shortIdandshortIdBatchtake it as a fourth argument for their random part;affinity,core-quest,inventory,npc-graph,character-blueprint,living-world-rulesand theplugin-with-toolstemplate pass it.@covel/plugin-test-utilsexportsmakeRandom, andmakeManualFunctionContextincludes it. A plugin test that builds its own context for a handler that rolls dice must addrandom. - A hidden story event can wait for a session turn. A condition takes
turnGteandturnLte:{ turnGte: 6 }holds from turn 6 on,{ turnLte: 12 }up to turn 12, both together are a window, and undernotthe event expires. World authors can write it in their hidden events (contract:story.events@1), and a plan (story-event.plan@1) can carry it. The story planner writes it as{ "afterTurns": 2 }, whichplan-story-eventsturns into the turn to wait for. Until now a consequence that comes "a few turns later" had no form of its own: both models we test made up an event to count from, which the tool refused, or wrote conditions that held at once.
Changed
- History compaction uses bounded segments through
history.compact@2, preserving earlier summaries until the total summary budget requires an oldest-segment merge. Custom compaction providers must implement the current v2 output contract. - Browser-private session heads and message-only frontend projections avoid repeatedly loading whole checkpoints or duplicating live plugin namespaces. BrowserVault schema 6 and the updated server journal schema require recreating affected development databases.
- Desktop production builds precompile framework/server TypeScript, start from the JavaScript entry, and accept a readiness IPC signal. Parent IPC loss drains the sidecar; package imports share server validation.
- A generated world is laid out as a hand-written one. The generator wrote the lore twice (
WORLD.mdand an identicalWORLD.<locale>.md), the characters tocharacters/main-cast.json(the conventional path of character cards, not of characters), each plugin record todata/contract-<n>.json, and adata/world.data.yamldescriptor to tie them together. It now writes oneWORLD.mdin the world's own language, the characters tocharacters/characters.json, and the records of a contract to the file its plugin names (data/memory-blocks.json,data/time.yaml), in that file's format. A package whose files are all at conventional paths has no descriptor and noworldDatainworld.yaml; a descriptor is still written for a contract whose plugin names no path. A session of such a world imports the same characters, lorebook, dimensions and plugin data as before.writeWorldPackagetakes the generation'sdataContracts. Worlds generated earlier keep working; revise one to rewrite it in the new layout. update-dimensionstakes one change at the empty path as the whole value. For a dimension that is a number or a text, which has no path inside it, a model wrotechanges: [{ path: "", value: 71 }]; the tool refused the empty path and the model sentvalue: 71. Seen in two of two sessions of a generated world, whose dimensions include plain numbers. Two changes with an empty path among them are still refused.initialize-worldno longer refuses definitions sent for a world that declares its own dimensions. The setup prompt asks the model to leavedefinitionsout when the world has author declarations, but nothing in the prompt says whether it has. For a generated world, which always declares dimensions, the model sent definitions, the tool refused the call, and the model sent it again: one model call lost at the start of every session (seen in two of two sessions of generated worlds, in none of bundled worlds, whose character schema lets the guard skip the agent). The tool now adopts the world's declarations and leaves the sent definitions out.listCharactersreturns characters by creation time, then by ID. The SQL backends had no order at all: SQLite returned insertion order through its index, and PostgreSQL returns rows as they lie on disk, where an updated row moves, so the roster in a prompt could change order after a character changed. MemoryStore returned insertion order. All three now return(createdAt, id), IDs compared byte by byte. Characters a world imports together keep the order the author wrote them in: the importer gives each one a creation time one millisecond after the one before it, where it gave them all the same instant, which listed them by ID (inemberbackthe first character of the cast file came last). A world-data sync that rewrites a character keeps its creation time, so the character stays where it was and a character new to the package follows the others. A session created before this change keeps its rows and lists such characters by ID.- Two turn messages of one millisecond list in pipeline order.
listTurnMessagesandlistUncompactedTurnMessagesordered bycreatedAtalone, which left the order of a tie to the engine. A tie now lists the player's message first, then by the stage of the runtime that wrote the message, then by ID. - An own-data line says the row key once. Most plugins store the key again as
idinside the value, so a line read- npc-1ac0c5fb | {"id":"npc-1ac0c5fb",…. The value'sidis left out when it equals the key. The roster ofchar-creatorand the session facts ofmemoryno longer carry a character's record version, which no tool takes from a model. - A model no longer reads a record's bookkeeping. The session ID, the UUIDs of rows, turns and results, and the times a row was written reached the model in several places: every own-data line carried the row's
updatedAt(- key | 2026-10-06T08:51:22.123Z | {…}), values carriedlastTurnId,evidenceTurnIds,unlockedAtand their like,{{ world.schema }}carriedsessionIdand two times, the memory extraction dumped a whole form record and a whole character record,get-characterprintedCreated:andUpdated:, and the JSON results ofplugin-data-get,plugin-data-listandmemory-searchcarried a time for every row. In one three-turn session that was 26 UUIDs, 66 timestamps and 9 session IDs; a second recording after this change had none. None of it is of use to a model: in 137 recorded answers no model wrote a UUID or a timestamp back, and no tool takes one. Each costs tokens, a timestamp changes the prompt whenever its row is rewritten, and none is the same in two runs of a session. The kernel now leaves them out of what it renders: own-data lines are- <key> | <value JSON>, and values in own-data lines,<runtime-inputs>,<runtime-exports>,{{ world.schema }}and JSON tool results lose a property when its name and its value both say bookkeeping (idor…Id/…Idswith a UUID,timestampor…Atwith an ISO time, andsessionId). Code reads the full record as before. The plugin SDK exports the function,modelFacingJson;memoryuses it for the text it builds itself. The prompts ofaffinityandcodexdescribe the new line. A plugin tool that hands the model a UUID in a JSON result must write it in_text. The rule reads the name of a property and the shape of its value, so it also takes a time of the story for bookkeeping when the property is named…Atortimestampand holds an ISO time (diedAt: "1943-06-01T08:00:00Z"): the model does not see it, and nothing reports that. Give such a property another name (date,diedOn) or write the time another way, in a plugin's data and in a world's. No bundled plugin or world has one. Seedocs/v2/reference/plugins.md. - Plugin-data lists have one order on every backend, and it no longer depends on a random row ID.
listPluginData,listPluginDataSessionScopeandlistPluginDataByNamespacereturned rows by(createdAt, id). Rows written in one commit share acreatedAt, and theiridis a random UUID, so the order of such rows differed from session to session and changed in a fork: the dimensions in<world-dimensions>came in a different order in every session. The order is now(createdAt, pluginId, namespace, key), compared byte by byte (PostgreSQL withCOLLATE "C"). MemoryStore returned insertion order; it now returns the same order. Within one session the order did not change from turn to turn before, and does not now. wordIdgives a label without ASCII words the same ID in every session. A Chinese name got eight random hex digits (npc-3fa9c1d2); it now gets eight hex digits of the name itself, numbered-2,-3when taken, as an ASCII name already was. IDs already stored stay valid.- The bundled bookkeeping and setup agents retry a failed model call three times.
affinity,codex,guide,world-ir,npc-graph's extractor,story-events' planner,world-time'sadvance, and the setup agentsplayer-initandschema-gendeclaredmaxRetries: 0: one call that timed out, lost its connection, or got an error from the provider failed the runtime, and that turn's record of facts, relations, or time was lost. The dimension tracker and the character tracker declared 1; a tracker that fails leaves a settlement pending, and the turns after it do not commit until it is resolved. All eleven now declare 3. In two eight-turn sessions through a proxy that made 28% of these calls fail, 19 of 184 model calls of these agents failed on the first attempt; with no retry each of them fails its runtime, with one retry 2 do, with three none (376 runtime runs, none failed). The time limits are the same (60 seconds for a call, 120 seconds for a call with its retries), so a call that never answers gets one more attempt and the runtime fails after 120 seconds where it failed after 60; a failure that comes back quickly gets up to three. A tool call that the tool rejects never counted against this number. - Action suggestions follow the story instead of a fixed set of types.
guidenow asks for 3–4 options (it asked for 3–6) and writes each as a different answer to the decision at hand. It no longer has to offer one option of each type; the type is a display tag chosen after the option is written. At least one option must change the situation (a new event, a changed relationship, leaving the scene), and at most two may only gather information. Thescene-prompts@1output is unchanged. - This turn's data follows the history. An agent's inputs (
<runtime-inputs>), exports, own-data blocks and the prompt segments that change every turn (position: "system"withvolatility: "turn", such as memory blocks and world dimensions) were part of the system prompt, ahead of the whole history. They changed every turn, so no request matched the previous one beyond that point and a provider's prefix cache never reached the history. They are now one system message between the history and the current turn, and the system prompt holds from turn to turn. Measured on qwen3.8-flash by replaying two consecutive requests with 60 more turns of history: the cache hits on the next turn went from 1,024 of 37,285 input tokens to 34,816 of 36,929 forguide, and from 3,072 of 35,731 to 31,744 of 36,001 for the narrator on a turn where no character changed. The role and the tag names are unchanged, and an adapter that lifts system messages to the top sends them in the system prompt as before. A hook that looked for these blocks in the first system message finds them in a later one. Two things still end the cached prefix early: a prompt body whose template variables change ({{ characters.npcs }},{{ player.character }}: the narrator's did in every turn of one session), and a history window (agent.history.maxTurns), which slides. Seedocs/v2/reference/prompt-structure.md. - The narrators' prompts no longer change with the characters.
narratorandchat-mode-narratorinterpolated the player's sheet into their prompt body, andnarratorthe NPC profiles too. Any change to a character changed the system prompt at that point: in one qwen3.8-flash session of 8 turns, in every turn, and the first request of each narrator turn had 9% of its input served from cache. Both plugins now provide the sheets as a turn-volatile prompt segment (<player-character>,<character-profiles>), which follows the history; the body names the blocks. In the same kind of session the narrator's system prompt was identical in every turn and 44% of that input came from cache; replayed with 60 more turns of history, 33,792 of 38,974 input tokens did. The plugin SDK exports the helper they use,characterSheetSegments.{{ player.character }}and{{ characters.npcs }}still work in a prompt body. - The prompt of a runtime that runs after the story has two more messages. The story text of the running turn follows the player message as an assistant message, then a short framework cue in the user role. A
PostContextAssemblyhook that rewritesmessagesfor apost-turnorauditagent sees them.agent.history.maxTurnsdoes not count the current turn. Seedocs/v2/reference/prompt-structure.md. - A Chinese session no longer reads English sentences that code puts into its prompt. The prompt body of a Simplified Chinese session is Chinese, but code wrote fixed English text around it: the heading of a world rule (
[World Rule: …]), the event instruction in<available-events>, the note under a history summary, the markers of pruned messages and of a cut tool result or summary, retry hints, the note that a draft was not accepted, the JSON Schema rule of a runtime withoutput.schema, and the notes of own-data blocks, dimension projections and character profiles. Each now has a Chinese variant chosen by the instruction language (instructionLocaleFor). The same for text that bundled plugins write: the extraction prompts ofmemory, the language rule ofhistory-compaction, the notes and the truncated heading of the twoworld-initdimension blocks, the cue instruction ofstory-events, the mood instruction ofsoundtrack, the receipt instruction oftabletop-rules, the cast block ofscene-stage, the read-limit message ofchar-creator, the narrative review's player line and corrections, and the regenerate prompt ofbranch-reply. Every other session reads the same English as before. Tag names, bracket tags such as[retry 1], ids and enum values are unchanged. Two texts that the narrative prompts name stay English in every session:No hidden story event this turn.andSettled tabletop check. Tool definitions and the results, rejections and validation errors of the tool loop stay English.characterSheetSegmentstakeslocalefor its one sentence; a model request carries the sessionlocaleto the adapter. Seedocs/v2/reference/i18n.md. - The correction for a narrative reply with no story names
runtime-done. The narrative review asked for the story with "Do not finish with only a tool call". A reply with no story is nearly always a bareruntime-donecall, and in a prompt of one language that sentence brought a story back in 8 of 72 replays on gpt-6-luna (6 of 48 in Chinese, 2 of 24 in English). It now says that the story text is missing, to write it as the content of the reply, and not to callruntime-donebefore it is written: 24 of 72 (18 of 48 in Chinese, 6 of 24 in English). Three replies with no story still fail the turn. - The framework's first lines of a prompt are in one language.
[RUNTIME]and[LANGUAGE]were always English and[COMPLETION]followed the session, so every agent of a Chinese session read a prompt that changed language twice before the plugin's own text. All three now follow the instruction language (instructionLocaleFor): Chinese for a Simplified Chinese session, English for every other. update-dimensionstakes noexpectedVersion. The version of an update is the one this execution read, and the tool already had it. Asking the model to copy it made two failures possible: leaving it out, and copying the number of an earlier turn. A version the model still writes is ignored. An entry of a dimension that the model names as a dimension (undercroft-song, a room ofdelveMap) is reported with the dimension it belongs to and the path to write.- The dimension tracker reads 80% of a text limit. A model does not count characters: asked for at most 200 it wrote 201, 219, and 245, and each such write was rejected and cost another model call. The schema it reads now shows
maxLength: 160for a limit of 200; the limit that is checked stays the author's. In replays of turns where a note was near its limit, calls with a text over the limit went from 5 of 56 to 1 of 56 on qwen3.8-flash. - A reply of
guidecan be plain text.generate-guidetakes a reply written as one sentence in place of{ text, kind }and shows it without a tag;kindis optional inscene-prompts@1. - A plugin's
authoring.source.pathtakes ASCII letters, digits,_,.,/and-only. It is the path a world package has the contract's file at, and the world generator writes to it. The manifest schema now refuses a leading/, a..segment, and every other character (a space, a file name that is not ASCII);authoring.examplealready had this rule. A plugin that names such a path loaded before and now fails validation withsource path must be a world-package-relative path: name an ASCII path in the plugin. A world that keeps the file at another path names that path in its own descriptor. Every bundled plugin passes. - Model data refreshed. The bundled LiteLLM snapshot is pinned to
be448177(2026-10-05, 4,472 models).
Removed
COVEL_LLM_REPLAYandCOVEL_LLM_REPLAY_DIR. They were registered as planned and no code read them. Recording and replaying model calls is done bypnpm llm:replay, outside the server process. Thedocumentedstatus of the environment registry, which only these two used, is gone with them.
Fixed
- A story cut at the model's output limit stays. A reply that ends at the output limit is an error for a tool call or JSON, which cannot be used cut. A story runtime that wrote only prose now keeps it: the text is committed, and the player reads a notice that it may stop mid-sentence. Before, the whole turn failed and the text the player had read was removed.
WORLD.mdand the size warnings ofvalidate:worldare measured in tokens. The story prompt carries up to about 8,000 estimated tokens of lore and cuts longer lore after the last line that fits. The limit was 8,000 characters, which cut the English editions ofharuka-academyandmistportto less than half while their Chinese originals fit. The warnings for constant lorebook entries (2,000) and initial dimension values (8,000) use the same measure.- A lorebook key matches its plural and possessive forms.
lanternmatcheslanternsandlantern's, and still notlanternfish. Whole-word matching had stopped matching the plural. - Claude requests keep the tool choice the player set. For the models that reject a forced tool choice (Claude Fable 5.1, Mythos 5.1, Opus 5.5, Sonnet 5.5) only
anyandtoolbecomeauto;noneandautoare sent as set. A conversation whose kept history starts with the model's turn gets a user turn first, which the Messages API requires. The per-model rules are in one place and follow Anthropic's API reference; they were not run against the live endpoint. - A fallback model that is too small is skipped, and the error names the first failure. The error of a failed call was the note that the backup could not fit the request; it is now the failure of the model that was tried, followed by that note.
- A private browser session no longer grows toward the upload limit. A checkpoint still carried about 0.6 MB per turn of execution journals and reached the 64 MB limit near turn 100. It now carries the results and runtime outputs of the latest 40 executions and the three trace rows the execution status is read from; the tool-call log and the event trail stay on the server. Messages, the prompt history and game state are complete.
- A plugin whose task cannot be loaded says so when it is approved. The server loads a plugin's approved runtimes as soon as the player allows its code and returns the ones that fail (
runtimeLoadErrors); the app shows them. Before, the failure appeared at the first turn that scheduled the task. - The desktop app clears the storage of earlier page origins. An install from before the port was kept had one origin per launch, each with its own media cache.
- World revision results now refuse publication after concurrent edits, deletion or replacement in server and browser-private storage. Browser-local revisions remain return-only even when the server holds a mirror, and initial browser generation refuses occupied world IDs. Package replacement preserves the old package for recovery if both publication and rollback fail; an incomplete generation stream is rejected.
- World-data sources can declare
localeArrayKeysfor nested translation identities; translation extraction preserves the selected identity field. Bundled memory-block translations follow their labels when reordered, and Mistport's English portraits inherit current media references from the main file. Manual handler test contexts accept the complete optional host capabilities. - Whole-settings edits persist additions, changes and removals atomically and reject a stale editing base. Late configuration-file reads cannot replace the selected file. Settings backups avoid same-millisecond collisions, hydration rejects misplaced secrets before making repair copies, and provider names that match inherited object keys survive secret reloads.
- Hosted configuration discovery omits operator-only paths and diagnostics from public responses. Unknown configuration failures use the global error boundary; malformed archives and refused character proposals retain input-error responses, and required state reads no longer return incomplete success.
- Media cleanup holds SQLite's write lock until content removal completes and refuses physical deletion inside an outer database transaction; metadata writes share the NUL normalization contract across all media backends. Literal outbound IP addresses use the same public-address policy as resolved hosts, including reserved and transitional IPv6 ranges, and text-model parameter overrides no longer leak into image API requests.
- Runtime exports use individual savepoints, so a failed diagnostic export cannot poison a PostgreSQL domain commit. Malformed optional-runtime domain effects are isolated from successful story commits. Long cooldowns allow a runtime's first execution, optional incompatible inputs are omitted, and input schema/pointer checks no longer reject valid branched patterns or accept non-canonical array indices.
- Repaired community packages that failed at startup can be rediscovered in development without executing unapproved code. Event schema validation follows the published plugin generation across hot reload.
- Character-sheet prompts filter internal bookkeeping fields and use compact JSON. Runtime input bindings, template paths, locale objects and test-runtime reports preserve explicit data keys without consulting or modifying inherited object properties. Test-runtime image exports allocate distinct filenames when record keys sanitize to the same name.
- State responses preserve explicit plugin-data and character-schema keys such as
__proto__. World authoring streams hide unknown internal failures while retaining conflict, validation and package-recovery diagnostics. - Invalid persisted desktop window dimensions and flags fall back to safe defaults; valid secondary-display coordinates still restore normally.
- Allocate PostgreSQL log positions atomically across connections, reject plugin-data batch CAS for missing sessions on every backend, and retain background-worker wake hints received during maintenance. Unexpected RPC and installation failures reach the global error handler without exposing production exception details.
- Embedding poison recovery retains actual HTTP error codes. A rejected input reported by an OpenAI-compatible endpoint no longer blocks later vector rows indefinitely. Truncated history summaries preserve the original history, and compaction verifies the canonical prefix before hiding source messages. Prompt admission reads only the latest form and a bounded oldest/recent plugin-data window, lore scanning includes the canonical recent tail, and losing a diagnostic trace reference leaves state replay intact.
- Queued actions recheck session ownership, deletion state and incarnation after acquiring their lock, and retain the corresponding machine-readable error code in SSE.
- Keep retry results durable after diagnostic trace cleanup, query execution records directly, share background job scans, and prevent diagnostic events from filling subscription replay buffers or being stored twice.
- Validate world presets, settings and data-source combinations; report unused nested files. Move free dimension editing into Debug and show settlement recovery during play. Price cache reads/writes when rates are known.
- World import previews validate direct and derived characters against the world schema and reject malformed or untriggerable lorebook records before any writes, with per-record locations and correction hints.
- Expose locale resolution, prompt loading, UI tool factories and dimension-provider helpers in the public plugin SDK; bundled plugin runtime code no longer imports host internals.
- Keep in-progress turn streams owned by their original action; remove uncommitted assistant output, deduplicate steering messages, and preserve same-timestamp message order after reload. Confirmation buttons and stage input respect focus and IME composition.
- Isolate plugin prompt/runtime loading failures; validate package file references, tool names, stages and contract dependencies consistently before installation. Preserve editor changes and deletion state when world translations finish.
- Reject truncated agent/world-generation output, preserve provider failure details, use streaming for all supported agent calls, and check the input budget before falling back to a smaller model. Anthropic caches transcript/tool history without moving late instructions; other protocols remove cache markers. Media submissions no longer retry automatically at the HTTP layer.
- Load localized WORLD.md into story context, preserve edited default lore, and use generic dimensions without reserved gameplay IDs. World generation resolves occupied IDs early and includes rejected answers in repair requests.
- Isolate invalid embedding inputs, improve CJK keyword recall and matching excerpts, and use filtered exact vector search with consistent L2 distance. Keep backend NUL handling, fractional lore ordering and deterministic text ordering consistent.
- Keep mobile panel state across drawer openings, expose panel retry and runtime skip reasons, debounce and bound execution display caches, and unmount folded historical cards. Detect model slots without usable credentials before presenting them as configured.
- Enforce desktop single-instance startup, preserve navigation during sidecar recovery, and route desktop imports through the shared validated installer. Hold SSE single-flight guards until asynchronous work finishes; background lock waits remain cancellable.
- Bound resume schema compilation caches, compact terminal dimension receipts, and avoid full hook prompt copies in rewrite traces. The guide runtime uses a short history window with persisted summaries.
- The world generator writes no file outside the package. A contract's records go to the path its plugin names (
contributes.data.<ns>.authoring.source.path), and that path was joined to the package directory unchecked:../escaped.jsonpassed manifest validation and was written next to the package. The manifest schema now takes only a world-package-relative path (no leading/, no..segment), and the writer refuses any path that leaves the package. - Two contracts that name one file no longer lose a contract's records in a generated world. The second contract's records replaced the first's, and no descriptor was written, while the importer reads a path that two contracts name for neither. The generator is now given a plugin's path only while it is a convention (no other contract of an installed plugin names it), and the writer gives a contract its own
data/contract-<n>.jsonwhen its path is taken by another source or is one of the package's own files (world.yaml,WORLD.md, the kernel's data files);data/world.data.yamlthen lists every source. - A resumed runtime keeps the whole turn under the context budget. A runtime that runs after the story protects two user messages of the current turn from the budget: the player's and the cue after the story. Only an ordinary run did; a resume protected one, so with a large resume payload the request went out without this turn's story and turn context, and the runtime still returned success. The count is now part of the suspension's continuation (
currentTurnUserMessages) and the tool loop applies it on every call. A player's interjection during a turn is counted the same way. A suspension saved before this change resumes as it did. - The tracker is shown no text limit below
minLength. The dimension tracker reads 80% of everymaxLength; a schema withminLength: 4, maxLength: 4was shown asminLength: 4, maxLength: 3, which no text meets. The shown limit is now at leastminLength, and a string withenumorconstkeeps the author's limit. - A retried provider request is sent at most four times. Retries after an HTTP 429/5xx and after a dropped connection share one limit of three, but a retry that a response asked for was counted only after it returned: HTTP 500 followed by a connection that stayed down made five requests. It is now counted before the request is sent again.
- MemoryStore orders plugin-data keys and character IDs as the SQL backends do, byte by byte. It compared UTF-16 code units, which put a character above U+FFFF (an emoji) before U+E000–U+FFFF (a fullwidth letter); SQLite and PostgreSQL order them the other way. With keys
Aand🐉written in one millisecond the two backends listed them in opposite order. The contract suite now checks this on every backend. - The dev server no longer starts on a plugin SDK build that is older than its source.
@covel/plugin-handlers-utilsis the one workspace package read fromdist/, and onlypnpm installbuilt it. After a pull that added an SDK export, the next server start loggednarrator: failed to activate entry … does not provide an export named 'characterSheetSegments'(and the same forchat-mode-narrator), kept listening and answered requests without those plugins. The server'sdevanddev:pgscripts now preloadscripts/ensure-plugin-sdk.mjs, which runs at every start of the server process, the watcher's restarts included: when a file under the SDK'ssrc/or its build config is newer thandist/index.js, it runs the package's build first. A change to the SDK source now restarts the server. A type error does not stop the start (the compiler reports it and still writes its output, as the rest of the workspace runs unchecked from source); a build that writes nothing stops the process with the command to run. - Opening the address of a session that no longer exists shows no error. The page went to the world list, as intended, but also showed the raw response (
404 /api/sessions/<id>and its JSON) as an error. Seen when a browser tab still pointed at a session after the database was removed. For this request a 404 is the answer "no such session"; any other failure is still shown. - A world-data sync no longer stops at the first conflict.
POST /api/worlds/:id/sync-data(andsync-dimensions) wrote nothing when any row was in conflict, and still answeredok: truewith the count of rows it would have written. A session that was played always has a conflict: its dimensions evolved, andforcedoes not override those. So a session never received what its world package gained after it was created, such as portraits: its character portrait tab said the world provided none. A row in conflict is now left as it is and listed inconflicts; every other row is written, updated or removed, andupserted/deletedcount what was done. Checked on a copy of a database with two sessions created before their worlds' portraits were imported undercharacter-blueprint: after one sync each had its portraits (7 and 6), with 13 and 10 evolved rows kept. - The image generation helper draws the random part of an image ID from
ctx.random, not fromMath.random. - MemoryStore keeps a rewritten plugin-data row's
idandcreatedAt. SQLite and PostgreSQL change the value andupdatedAtonly; MemoryStore replaced the whole row, socreatedAtmoved to the last write. The contract suite now checks all three. - A story runtime is no longer told to finish with
runtime-done. The framework's[COMPLETION]line told every runtime without an output schema to callruntime-done, and the tool was in the narrator's tool list. A model that follows instructions to the letter called it and wrote no story: on a GPT-6 model 6 of 33 story runs failed, and the others needed about three model calls each. A story runtime now reads that its result is the story text of its reply, andruntime-doneis not in its list; in two sessions of 18 story runs none failed, with under two model calls each. A model that still calls it is asked for the story, as before. - A refused or dropped connection is retried. The transport retried a 429 or 5xx response and nothing else, so one moment in which an endpoint was restarting failed the runtime call at once. A connection that was refused or closed before it answered is now sent again with the same backoff and the same limit (three retries in all). A timeout is not retried there.
- A reasoning level reaches GPT-6 models. A
reasoningEffortset on a slot was dropped without a word for a model namedgpt-6…: the name was taken for the OpenAI family but for no known model of it. GPT-6 and later are known by name, with the levelsnonetoxhighandmax. - An empty
upsert-npc-graphcall means nothing changed. The tool refused a call with no node and no edge and told the model to callruntime-done; the model did, and the runtime failed all the same, becauseruntime-donedoes not settle a failed call. The call now succeeds and writes nothing. The same holds for a call that held only places and items. - Three more slips of the story planner no longer cost another model call. A plan without
reasonis taken: the reason is a note for debugging that nothing reads. An event planned under the ID of an event that already fired is what follows that event, and it gets the next free ID (meg-offer-2); a fired event is still never planned again. A text among the events ("reason ="after the last event, from a broken end) is left out when the call holds an event. - The story planner's calls are rejected far less often. In replays of 15 recorded turns
plan-story-eventsrejected 18 of 90 calls on qwen3.8-flash and 23 of 90 on deepseek-flash; with this change 5 and 3. Its prompt now gives the four forms of a condition and says that only story events have IDs forrevealed: the models had taken things that happened in the narrative, and entries of World IR, for events. The tool settles what has one meaning (aturnsSince*bound written as an entry of its own, a condition written on the event,noneinsideall, a field given asnull,notExists, an operator and its value as two keys,dimension: "worldTime"on a world-time condition, a path where the dimension ID goes, a number comparison on an object with one number field) and leaves out an "unless that event fired" guard on an event that does not exist. It now also refuses a condition that the schema of a dimension can never meet (a field that is not there, a number comparison on text, a value outside an enum), with the fields that can be used: until now such an event was accepted and waited forever in one of the eight places for planned events. - Three more slips in a model's tool arguments no longer cost another model call. A closing bracket that does not match the open one is left out, or at the very end replaced by the right one (
[{"id":"a"},{"id":"b"}}]). The opening of an object written twice is written once ({"{"id": …). In an array or object sent as text inside a field, brackets still open at the end are closed: the arguments around it parsed, so the model ended the text itself. Each is accepted only when the text then parses with no other change. On qwen3.8-flash, 36 of 1,695 replayed calls had broken JSON, 35 of them inside a field; 4 were usable before and 20 are now. - A tool whose schema has a Unicode-property pattern no longer fails on APIs that check schemas.
upsert-npc-graphdeclared its relation label with\p{Letter}; an OpenAI-style API answered every request of the NPC graph extractor with HTTP 400 ("is not a 'regex'").tool()now leaves such apatternout of the JSON Schema it sends, and Zod still checks the value. The relation rule is checked in the tool and stated in the field's description. submit-world-factssettles three more slips. A relation, event, or statement without anidgets one from its content. A reference with the letters of exactly one session character's handle means that character (雷恩-修女for雷恩修女, or the name itself). A statement subject that names an event of the same output is left out: subjects are entities. Over 627 replayed calls on qwen3.8-flash in Chinese, 107 were rejected before and 60 are now, and no call that was accepted is rejected. The id examples of its prompt no longer name an item and a person of a bundled world.- Agents that run after the story no longer answer the previous turn (ackness/covel#139). The conversation sent to a
post-turnorauditagent stopped at the previous narrative and the new player message: the committed history ends one turn back, and the story just written was only in a JSON block of the system prompt, ahead of the whole history. As the history grew, the model took the end of the conversation for the present. Action suggestions then recapped the previous turn and offered what the player had just done. The kernel now adds the story text of the running execution to the conversation, as the next turn's history will show it, so every such agent reads the turn it is working on. A retried runtime already read it from the committed history; both paths now send the same conversation. - Action suggestions do not repeat what just happened.
guidereads the last story reply as the present moment, writes its recap up to that point, and does not offer an action the narrative completed or a question it answered. - Three slips in a model's tool arguments no longer cost another model call. They are settled in the whole argument string and in an array or object sent as text inside a field. Closing brackets after complete arguments (
{"updates":[…]}]}) are dropped; until now this was done only for text inside a field, and the same slip in the whole argument string failed withINVALID_ARGS. Quote marks that a model writes bare around a word or a line of speech inside a string value ("note": "The page calls it a "lock" and not a lamp.") are escaped, only where they come in pairs and never in a key. An array element left open where the next one starts ([{"id":"a","attributes":{…}, {"id":"b"}]) is closed, only when the text then parses with no other change. Every other fault is reported as before: an array that was not closed, a key that was left out, output that stopped early, a missing comma. Seedocs/v2/reference/tools.md. - The NPC graph no longer takes places and items for people. The extractor's prompt said that the
entitiesof its input are people, groups, and factions. They also hold places, items, skills, and concepts, and the model wrote those as nodes: about a third of all nodes in replays of recorded turns, most of them labelledgrouporindividualto fit the tool. The prompt now says what the entities are and that only people, groups, and factions are nodes.upsert-npc-graphreads thecharactertype asindividual, leaves out a node typedlocation,item,skill, orconceptinstead of rejecting the call, and skips an edge to it. Nodes of that kind that an earlier turn recorded stay in a session's graph. submit-world-factsaccepts two more forms of reference. A reference that holds the name of an entity of the same output, where its id goes, means that entity when no other entity has the name. A reference to an item or quest that the session already tracks (a name of theworld-ir.vocabulary@1input) is declared from the vocabulary, as a session character is declared from the roster. On qwen3.8-flash these were 3 of the 7 recorded rejections for an undeclared reference.
Breaking changes and upgrade notes for v0.0.48
- This version does not read the data of an earlier version, and does not migrate it. Sessions saved by an earlier version are lost. Covel is in early development and keeps no migration code for stored data.
- Server database:
messages,trace_eventsandturn_resultshave new columns. An earlier SQLite database opens, and nothing reports a problem at startup; then every chat read and write fails withno such column: seq. Delete the database file and its-waland-shmfiles before starting this version:<covelHome>/data/covel.db(typically~/.covel/data/covel.db) for the desktop app,./data/covel.dbfor a development server without a Covel home. For PostgreSQL, recreate the schema. - Browser-private saves: BrowserVault is schema 6 and refuses an earlier vault. Delete the site's IndexedDB database
covel-browser-vault. history.compact@1ishistory.compact@2: the output is a list of summary segments, each with the summaries it replaces. A compaction provider written for@1is not called.- Prompt templates:
{{ world.tone }},{{ world.openingScenario }}and{{ session.id }}are removed. Read the tone and the opening from<world-lore>and the dimension projection; name a turn by its number. - Hooks: a handler that returns nothing continues. A handler that throws or times out vetoes only on
TurnStart,PreCompaction,PreRuntime,PreToolUseandPreStateCommit; on every other event it is skipped. - Plugin packages: the loader rejects a runtime whose declared schema file is missing.
pnpm validate:pluginand the installer also reject a referenced file that does not exist, anautoorscheduledruntime without a stage, an unknown framework tool name, and a plugin tool thatcontributes.toolsdoes not list. - World packages: the import preview and
pnpm validate:worldreject a selective lorebook entry without a key, a lorebook record withoutcontent, and a character that does not fitcharacterSchema.pnpm validate:worldalso rejects an unknownpresetIdand an invalid plugin setting. create-charactergives a word ID (char-lin-yao) in place of a UUID.agent.loop.callTimeoutMslimits a call that is not streamed. Every agent call streams when the model adapter can; such a call is limited byfirstTokenTimeoutMsandidleTimeoutMs.- Desktop: the IPC channels
covel:import:pick-pluginandcovel:import:pick-worldare removed. The import menus open the installer in Settings. - Plugin and world authors who upgrade from before v0.0.47 also read that release's upgrade notes.
- macOS Apple Silicon and Windows x64 artifacts remain unsigned, and macOS artifacts are not notarized. First launch may show Gatekeeper or SmartScreen warnings.
[0.0.47] - 2026-10-05
This release adds world galleries, scene- and mood-based background music, and author credits, and fixes world storage selection, background model readiness, runtime scheduling, and turn settlement.
Added
- World gallery. The art a world package ships shows before a session exists. In the
showcaseworld list the selected world's scenes rotate behind its title — with a pause control, and without starting for a player who asked for reduced motion — and its portraits sit under the summary; a picture opens at full size. The world details list every picture. A world without a built-in cover opens on its own scenes. A package says what its gallery holds inmedia/gallery.json: the pictures, their order, what each is (hero,map,scene,still,portrait), and a name and an introduction in every language of the package; the gallery shows them with the picture, up to the opening. A package without that file shows the raster images of its publickind: mediasources. New routes:GET /api/worlds/:id/galleryandGET /api/worlds/:id/gallery/:source/:file, which serve only listed files. - World art on the cards. In the
cardsworld list a cover shows a few faces of the world and the number of its pictures; they open the gallery. The world's scenes pass over the cover while the pointer rests on it. The "continue" banner rotates the scenes of the world and lists its pictures. - Background music. A world can ship music: files under
media/music/and a track listmedia/music.yamlthat says when each track plays — in which scenes, for which moods, or as the theme of the world. The moods are the author's own words: the track list names them and says when each applies, and the narrative is told that list. The new bundledsoundtrackplugin records the mood (the newmusic.cueevent, emitted by the narrative) and the scene (scene.set), and chooses the track. The kernel UI slotstage.music@1carries the choice to the app, which plays it: tracks change with a crossfade, the music lowers itself under a narrated line, pauses while the page is hidden, and has a switch in the session header and a volume in Settings → General. Other plugins can provide the same slot. Seedocs/v2/reference/world-data.md(Background Music) anddocs/v2/reference/ui-panels.md(Stage music). - Theme music on the world list.
world.yamltakesthemeMusic: an.mp3or.wavfile undermedia/. The world list (in theshowcasearrangement) and the world details play it for the world they show, with the same switch and volume as session music. No bundled world ships one yet. - Music generation API. Function runtimes get
ctx.music.generate()andctx.music.isAvailable(), the gateway getscomposeMusic, model slots get themusictag, and a plugin registers a provider's format withcovel.registerWires({ music }). A music request resolves themusicrole and never falls back to a speech or a text slot. The framework ships no music wire and no plugin uses the API yet: it is the place a provider plugs into. - A world list of any length. The strip of covers in the
showcaseworld list stays one row: what does not fit scrolls with a faded edge, steps to either side, and the arrow keys; the selected world stays in view; "All worlds" opens a searchable grid. - Lantern Barrow now ships six full-body character portraits and a dedicated world-selection cover. Emberback adds portraits for Priya Nair and Eli Varga. The four active worlds include a first gallery asset collection with localized introductions, setting background, source references and clearly identified illustrative scenes.
- Gallery descriptions now also cover 35 reviewed existing portraits, expression and outfit variants, day/night scenes and world covers. Each includes Chinese and English descriptions grounded in the original setting, with visual observations separated from story facts; the existing image files are preserved.
- Credits for plugins, worlds and collections.
PLUGIN.md,world.yamlandcovel-collection.yamltake the same optional fields:author(name,url, a shortaboutmessage, and up to six labelledlinks),licenseandhomepage. World cards show the author and version; the world page, the session preparation screen and the installed-plugin list show the full credits, and an install preview shows the author's name. The play view does not show them.aboutand link labels are translated like other labels (locales/<locale>.yaml,world.<locale>.yaml); the author's name and the license are shown as written. Fields and limits:docs/v2/reference/plugins.md. - A warning before a third-party link opens. Links in credits are
httpsonly. A click shows the full address and its host and says the link comes from the package author, not from Covel; it opens after the player confirms. - Versions on world cards and bundled plugins. A world's
versionnow reaches its cards (metadata.packageInfo), and every bundled plugin states itsversion,authorandlicense.pnpm validate:pluginfails when a plugin'sPLUGIN.mdandpackage.jsonstate different versions.
Changed
Stage is the default appearance for new profiles. First launch uses the scene-based Stage layout and world showcase. Saved appearance choices are preserved.
Panel tabs that do not fit. With labelled tabs (
panelTabs: "bar") the context panel no longer shows a scrollbar under a cut-off row. The cut-off side fades, the mouse wheel moves the row, the selected tab stays in the middle, and a button at the end lists every panel with its full name.Existing sessions run plugin setup once more. The bundled plugins had no
versionand counted as0.0.0; setup completion is recorded per version, so each setup runtime runs again on the next turn of an existing session and skips work that is done.pregameno longer repeats its welcome in a session that is already playing. A session whose world changed its dimension definitions after the session began can fail this rerun; use the setup recovery control in the plugin panel, or start a new session.GithubPluginPreviewand the collection preview carryauthor(a name ornull).Losslessly compress bundled world art, documentation screenshots and application icons. Image dimensions, decoded pixels and transparency are preserved, and world media hashes and byte counts are refreshed. Already compact assets remain unchanged when re-encoding offers no meaningful saving.
Fixed
Partial regional plugin translations keep available language translations. Plugin text written into session state and model context now resolves each message by exact locale, then the same language and script, then its English source. A partial regional catalog no longer hides the rest of its language catalog.
Selected panel tabs stay visible after layout changes. The active label can grow as its font weight changes, and the panel menu or a late font can resize the strip after the initial scroll. The tab strip now follows those size changes so the selected tab is not clipped.
Switching gameplay packs replaces the previous pack's requests. Selecting the traditional pack after the dialogue pack kept the dialogue narrator enabled, and selecting the smaller pack kept optional plugins from the previous pack. Pack requests now have their own source, retained through manual edits; switching replaces that source while preserving other defaults, manual additions and explicit exclusions. A default that the world's initial pack requests belongs to that pack even when the world's preferred tags also select it, so another pack can replace the world's default narrative engine.
Synchronous event subscribers honor their dependencies. A legal subscriber with
needsor a required input from another subscriber in the same fan-out batch was skipped because the batch ran in parallel. Matching synchronous subscribers now run in DAG levels with results merged between levels. Cycles are skipped while independent branches continue; background jobs remain independent.Late background handoffs keep the job's latest status. A delayed queued status or
runtime.deferredannouncement could replace a completed background row with a waiting row. The client now checks the durable job's control sequence and keeps its terminal state during live updates and trace recovery, while allowing a new job to start and a plugin's reported success to be corrected after a failed commit. The row keeps the handoff's start time whichever event arrives first.World generation waits for its save destination. Starting generation before server storage discovery finished could save a browser-private world to the server's world directory. Generation now resolves storage before contacting the model, reports discovery failures for retry, and respects cancellation while waiting. Regenerating a world with the same ID replaces its list entry instead of adding a duplicate card. Browser tests use a temporary user profile and assert that browser-private generation stays in the browser.
Detached jobs were judged by the default preset, not the slot they call. A detached function runtime with no declared model slot (the story-memory
extractruntime) had its credential readiness resolved against the system default preset, which ignores request-level UI slot bindings. On a desktop setup whose models are configured only in Settings (nollm.toml), the check never passed: the job sat queued for its fullmaxQueueMs(5 minutes), died asqueue-deadline-exceeded, and every following turn stalled for the 30-second settle wait. Function runtimes can now declarefunction.model(the slot theirctx.gatewaycalls use);memory/extractdeclaresmodel: memory.Dimension initialization obeys the commit policy. With a committed story, an optional runtime's
dimension.initializewas written beforePreStateCommithooks and outside its own savepoint, so a vetoed or rewritten initialization still left the value first proposed. Initialization now has one write path, its own proposal, and settlement receipts are built from the records that did commit.A malformed UI part no longer discards the turn. An optional runtime that returned
effects.uiwith a non-object part made commit preparation throw and rolled the story back with it. Such an entry (also ineffects.interactionsandeffects.notifications) is now a rejected write of that runtime alone; a function runtime fails withoutput-schema-invalidwhen its handler returns one.A resumed runtime keeps its turn. After suspend and resume, tools and function handlers lost
ctx.locale,ctx.logicalTurnand plugin translations, and agent tools lostctx.world, so the built-in dimension tools failed. The continuation now carries the locale and clock of the suspended execution, and a resumed agent reads the World Model like any other run.A failed tool is never the required work. An agent with
completion.require: tool-usecould finish without doing the work after a business tool had failed: by callingruntime-done, by reaching its step limit or being stopped by aPostToolUsehook while its reply carried a valid output, or, after suspend and resume, with a text answer. The contract is now judged where the loop returns, so every way out asks for a business tool that succeeded, in this run or before the suspension.ctx.musicreaches function handlers. The runner built the music capability and did not pass it on.Disabling the plugin that holds a session's dimensions is refused. It used to succeed and then leave a browser-private session unable to save or restore its checkpoint. Enable and disable now answer
409 dimension_provider_requiredwhen the change would drop the bound provider.A deleted session receives no late job status. The background worker could write an old job's status into a session deleted, or re-created under the same id, in the meantime — when it claimed a job as well as when it reconciled finished ones. The status row is now written in one transaction with the check that the job and the session still exist.
A fork that cannot keep its dimension provider is refused. Forking a session whose dimensions belong to a community plugin produced a child that could not be opened or saved, since a fork does not carry the plugin's approval. The fork now answers
409 dimension_provider_requiredwhile the snapshot holds dimension data, and creates an unbound child when it holds none.A pending settlement protects dimensions initialized in play.
sync-dimensionscould redefine such a dimension, which has no import ledger row, while a narrative was still waiting to be settled.Hidden story events:
noton an unavailable reference. A condition such asnot: { time: phase, equals: 3 }held in a session without world time, revealing the event early. A leaf that cannot be judged now stays undecided throughnot;anyandallstill use their decidable branches. Localized event text follows the shared rule: the session's locale, the same language and script, then English asen-USreads it.An optional plugin that is not installed no longer blocks its consumers. A runtime with an optional input that accepts a contract (
accepts: "contract:world-ir@1") failed to load, and every turn with it, when no installed package published that contract. The runtime now loads and the unchecked value is withheld.One plugin folder in a retired layout no longer stops the server. The desktop folder import accepted a package whose runtime manifests were still named
PLUGIN.md, and discovery then failed for every plugin. The import refuses that layout, and discovery reports such a package as its own load error.Writes from another site are refused. A browser's "simple" cross-site request could reach a write route of a local server, since CORS only withholds the response.
POST,PUT,PATCHandDELETErequests with anOriginthat is neither allow-listed nor the server's own host now get403 origin_not_allowed.A setup runtime that pauses now completes when it is resumed. A setup runtime that suspended for the player's input and reported
completion: doneafter the resume committed its writes, but its setup record stayedpendingand the session stayed in the setup phase, so the next action ran the setup again. The commit that resolves the suspension now records the completion and, when it was the last setup runtime, moves the session toplaying; the pause and its resume count as one attempt. No bundled plugin pauses during setup.A satisfied setup dependency is no longer reported as a cycle. With
needsofscope: sessionon a contract, a setup runtime whose need was already met by a completed provider was markedblocked(setup-session-cycle) when another provider of the same contract waited on it. The check now reads completed providers, and acardinality: oneneed is released by any provider that can complete; only runtimes that no order can release are blocked. No bundled plugin declares such a need.A turn without its story is not committed. When the plugin that provides dimensions failed in a turn, the narrative was skipped but the turn still committed: the writes of the runtimes before it were kept (a story event could be marked as revealed and never told) and the turn was counted. Such a turn now fails as a whole, like a failed story, and the player can retry the action. The bundled dimension provider does not fail this way.
Removed
- Unused code with no caller: the fixed world-detail sections of the web app, the earlier server-restart probe (
ensureServerSession), the whole-table provider-key andmessages/syncwrappers of the web API client, the earlier portrait replace helper, and the desktop copy of the provider-key helpers (it now uses@covel/shared).createSlotRegistry()no longer takes the preset registry it never read.
Upgrade notes for v0.0.47
- Bundled plugins now declare versions. Existing sessions run their setup checks again on the next turn and skip completed work. If the world changed its dimension definitions, use the setup recovery control in the plugin panel or start a new session.
- World theme music requires a package-provided audio file; no bundled world includes one yet. Music generation exposes an API but ships no provider wire.
- When upgrading from before v0.0.46, also read that release's breaking contracts and upgrade notes.
- macOS Apple Silicon and Windows x64 artifacts remain unsigned, and macOS artifacts are not notarized. First launch may show Gatekeeper or SmartScreen warnings.
[0.0.46] - 2026-10-04
This release adds style schemes that change the layout as well as the colours, puts each language of a world or plugin in its own file with tooling to translate it, writes an AI-generated world one part at a time under a limit on silence instead of total time, and keeps Settings usable when an upgrade meets older settings (#117–#121, #123–#127).
Added
- Configuration files can be edited in the app. Settings → Configuration files shows the raw configuration as text: the settings of this device as JSON,
llm.toml, andconfig.tomlin the desktop app. A save is checked first — the server's own parser for the TOML files, the registered schema for each setting — and a text that fails is not written. The version before the change is kept (llm.toml.bak,settings.edit.bak.json),llm.tomlis applied at once, and an earlier copy of the settings can be loaded into the editor. New routes:GET /api/config/raw,GET/PUT /api/config/raw/:name, behind the guard of the install endpoints. - Style schemes. A theme package can now change structure as well as colour: it names a
layoutpreset (classic,book,stage,panel) and may override where navigation sits, how the context panel shows its tabs, what the session does with world art, how the world list is arranged, and where a turn's results go. Three built-in schemes use it — Panel, Book and Stage — and Settings → Appearance has a style picker. The earlier themes are colourways of a fourth style, Classic, through the newgroupfield. Seedocs/v2/reference/theme-packages.md. - One rendering for options. The catalog gains
ChoiceListandChoice. Kernelchoiceblocks, the reply variants ofCandidateList, and the stage decision panel draw options the same way, so each scheme shows them one way: tagged cards, a numbered list, or key-capped rows. Number keys 1–9 pick the n-th option of the turn being played. - Session summary. The kernel UI slot
session.summary@1lets a plugin put one to three lines — text, a gauge, or a short list — where the active layout keeps status. The bundled world-time, core-quest and inventory plugins provide the time, the next open objective with its progress, and the carried items.appendSummaryEntriesin@covel/plugin-handlers-utilsbuilds the result. - Status at a glance. The player's bounded
statsshow as gauges, unbounded ones as numbers, andequipmentstring lists as carried items: under the panel tabs, or on a scene HUD beside the cast when the layout shows the scene. The world list offers "continue" for each world's latest session. - Events from plugin UI.
POST /api/sessions/:id/plugin-rpcacceptskind: "event", and catalog specs and widgets get anemitEventaction. A plugin's UI can emit the topics that plugin declares; the runtimes that subscribe run as background jobs. Community plugins are asked for the event grant and for each subscriber's runtime grant. - Plugin widgets in the new layouts. A sandboxed
webviewreceives the active scheme asthemeand as--covel-*custom properties; a side panel can queue a line for the composer withdraftMessage; and every plugin panel can be opened in a large dialog. - Clickable map example.
tests/third-party/clickable-mapis an installable example of a self-drawn panel that travels on a click.pnpm e2e:extensionsplays it in a real browser. - One language per file, for worlds and plugins. A world's main files hold its own language;
<name>.<locale>.<ext>beside a file holds only the translated text under the same keys and ids. A plugin'sPLUGIN.md,RUNTIME.md, UI specs and handler code hold English;locales/<locale>.yamltranslates manifest labels (one section per manifest file) and other text (messages, keyed by the English text). Handlers read text withtranslate(ctx, "…")andlabelText(ctx, "…")from@covel/plugin-handlers-utils. Rules:docs/v2/reference/i18n.md. - Translation tooling.
pnpm i18nworks on a plugin or a world directory:statuscounts what each language has and lists what it lacks,extractprints the missing entries,translateasks the configured model for what is missing or stale and checks every reply,lockrecords the English text each plugin label translation belongs to. A world can also be translated from its detail page ("Translate to …",POST /api/worlds/:id/translate). - Translations outside a package.
$COVEL_HOME/translations/plugins/<id>/<locale>.yaml(COVEL_USER_TRANSLATIONS_DIR) fills what a plugin's own locale file does not translate: labels and messages only, never a prompt. - Language tiers. English and Simplified Chinese have their own instruction text; every other language gets interface and content with English instructions and is marked experimental in the language switchers. A plugin never fails for a language it lacks:
validate:plugin, the plugin summary (languages) and the session prep row show what it has, andPOST /api/sessionsreturnspluginsWithoutLocale. - Chinese edition of Emberback, full English edition of Haruka Academy.
- Revise a generated world in one sentence. The world detail page takes a request such as "add a fourth character" for a world the generator made (
POST /api/ai/revise-world). The model writes only what changes; the server merges it into the current world by key and by item id, and the result passes the same checks as a new world. - World content from plugin declarations. A plugin that accepts world data says how to author it (
contributes.data.<namespace>.authoring: title, hint, example, conventional source, and whether the generator offers it). The world creation dialog lists the offered content fromGET /api/framework/authoring, andpnpm describe:authoringprints what a world may contain for the scanned plugins, with a ready descriptor entry and example for each contract. - Whole-package world validation.
pnpm validate:worldruns the session-creation preflight: every record of every contract source is checked by the receiving plugin's schema, for each declared language, with plugin IDs, settings keys, lore files and locale files. It returns structured diagnostics;--strictturns warnings into errors. - Generated schema reference. Every author-facing field of the world manifest, world data descriptor, dimension definitions, plugin manifest and runtime manifest has a description in its Zod schema.
pnpm schemas:generatewrites the JSON Schemas anddocs/v2/reference/schema/*.md;pnpm checkfails when a page is stale. The bundledworld.yamlfiles name their schema, so editors offer completion. - Prompt gate.
pnpm check:prompts(inpnpm check) fails when a canonical prompt is not English, when a variant other than*.zh.mdexists, when the two languages name different tools, blocks or inputs, or when the English text changed and the Chinese did not (pnpm prompts:lockrecords a pair). It also reports style findings againstdocs/v2/plugins/prompt-style.md. - World generation shows what it is writing. The creation dialog and the revision panel list each part of the world — settings, lore, characters, lorebook, rules, and each kind of plugin content — as written, being written (with the number of characters received and the attempt), waiting, or not generated.
POST /api/ai/generate-worldandrevise-worldcarry this inpartson theirprogressframes. - The wait for the model is a setting. Settings → General has "World authoring: wait for the model" (15–1800 seconds, 120 by default), sent as
idleTimeoutMswith world creation, revision and translation. The API accepts a wait from one second, so a test of the timeout path takes seconds. When a request ends because the model stayed silent, the error names the setting (code: "model_idle_timeout"in the error frame). - Browser test for AI world generation without a real model.
tests/e2e/world-generation-progress.spec.tsdrives the creation dialog against a provider that runs in the test process (tests/e2e/helpers/fake-llm-provider.ts): the part list, a slow answer that outlasts the idle timeout, a model that stops answering, and a world that falls short of the brief. The four tests take about 14 seconds and run inpnpm e2e:smoke. Seedocs/v2/internals/e2e-testing.md. - Real-model harness reports.
pnpm e2e:verifytakes--locale, counts rejected tool calls by runtime and tool, checks the language of what models wrote against the session language, and reports text of another script that was sent to a model.
Changed
- Settings dialog and its entry points follow the theme. The navigation has an icon per page; General and each plugin page open with a title and one line of context, one setting per row. Controls use the theme's corner radius, rule colour and accent. A switch sits beside its text, and a number field is as wide as a number. On the world list, "AI Create World" and "Configure Providers & Models" are two rows in one frame, each with its full text. In the classic layouts the studio panel lists each model role with its model, and "Settings" and "New session" are plain rows with a gear in place of the key.
- Earlier settings that this version cannot use no longer lock the settings. One of two things made every setting read-only: a
settings.json(desktop) orcovel:settingsentry (browser) in the v1 format, without a version or damaged; or one stored value that an upgrade no longer accepts, such as a removed option or a plugin setting with a new range. The app now keeps a copy first. An unusable file is moved aside (settings.v1.bak.json,covel:settings.v1.bak) and the settings start from their defaults. For single values, the file is copied (settings.conflict.bak.json) and only those values go back to their defaults. A notice names the settings to enter again and the copy, and Settings → Data shows and downloads the copies. API keys are not affected. A file from a later version is still left in place and read-only. - Model Roles page is shorter. The page opens with one block (the active
llm.tomland "Reload config"); the notes are folded. A role card no longer repeats the provider and model under the two pickers, and its two actions share a row. With more than six roles a filter box narrows the list. The model picker names the configured model "llm.toml default · …" where it said "Use default · …". - Plugin settings show what the current world sets. For a setting you have not changed, the page shows the value from the world being prepared or played, and names the world. It showed the plugin's own default, which that world does not use.
- Operator Access appears only where a token is checked.
GET /api/healthreportsoperatorTokenRequired. Settings hides the page on a server that reportsfalse, unless this browser holds a token. - Language lists name each language in that language ("English", "Русский"), in Settings, the top bar and the getting-started guide.
- The Data page says what each action covers. Reset names what it clears: every setting and every saved API key on this device, with the providers, models and themes you added.
- The world list has a "Settings" link beside the guide link. "Configure Providers & Models" still opens the providers page.
- Settings fields save when you leave them. Text, number, slider and API key fields write on blur or Enter, not on each key press. A number outside its range becomes the nearest allowed value, and the range is shown. A setting you have changed shows "Use default"; for a plugin setting this is how it follows the world or the plugin again.
- A new world is written one part at a time. The manifest, the lore, each requested supplement and each plugin data contract are separate model requests; a later part is given the parts already written. A part that cannot be imported is asked for again alone, up to three times, instead of the whole package being generated again. A supplement that still fails is left out with a warning and the world is created without it; only the manifest and the lore are required. A revision stays one request.
- World authoring is limited by silence, not by total time. A request ends when the model sends no text and no reasoning for the idle timeout. A model that keeps writing is not cut off; one request may run for 30 minutes. A request that ended in silence is not sent again: generation stops, or finishes with the parts it has when only a supplement was missing, so a model that does not answer costs one wait and not three.
createWorldtakesidleTimeoutMsandonProgress; itsattemptTimeoutMsoption is removed. - Plugin scaffolds outside the repo have no install step.
pnpm create-plugin <name>no longer writestsconfig.json,pnpm-workspace.yaml, apreinstallscript, or a dev dependency on@covel/plugin-handlers-utils, which is a workspace package and is not on npm. Check a scaffolded plugin withpnpm validate:plugin <dir>andpnpm test:runtime.--with-toolsplugins, which live in the repo workspace, keeppnpm lintand are now named@covel/plugin-<name>like the bundled plugins. - Issue and pull request templates follow the current workflow. The PR template lists
pnpm check, the database, UI, package and real-model checks, a "not verified" note, and the changelog; the bug report asks for the run mode, storage, world, community plugins and where to find logs. - Panel is the default look. A browser with no saved appearance opens in the Panel scheme; a saved choice is kept.
- Session toolbar. The world, turn and current scene sit on the left; the text/stage switch, settings and the panel toggle on the right. Detailed and raw views, studio configuration, "back to setup" and traces moved into an overflow menu.
- Turn results open for the latest turn in the Panel and Stage schemes, and sit in the page margin in Book. Classic keeps them folded.
- English is the instruction language; Simplified Chinese is the one variant. The canonical
PLUGIN.md/RUNTIME.mdbody is English and*.zh.mdis read for Simplified Chinese sessions; every other session reads the English body and answers in its own language.COVEL_INSTRUCTION_LOCALE(en/zh) fixes the instruction language for all sessions. Breaking:PLUGIN.en.mdand variants for other languages are no longer read; a plugin whose canonical body is Chinese must move it to*.zh.mdand supply an English body. - A session's language is fixed when it is created and must be an edition the world has; otherwise the session uses the world's own language and the response carries
requestedLocale. Breaking:POST /api/actionsrejects a request that carrieslocalewith 400. - World packages hold one language per file. Breaking: an inline locale map in
world.yaml, a dimension file or another structured source is rejected (inline-locale-map); move every language but the default one into a locale file. A session stores dimension values in its own language, andworld-dimension-getno longer takesresolveI18n. A session created earlier is converted when it is opened (see Fixed). - Plugin packages hold English, with translations in
locales/.validate:pluginrejects an inline locale map in a manifest or a UI spec. Built-in and plugin tool definitions are English. - A world needs no data descriptor. Without
worldDatainworld.yaml, files at well-known paths are the sources (data/dimensions.yaml,data/lorebook.yaml,characters/characters.json, and the path each plugin names for its data). A source'sschemadefaults to the one of its destination. The smallest world isworld.yamlwith five fields andWORLD.md. Breaking: recreate development sessions of Emberback, whose source IDs changed. - Plugin manifests repeat less. A single-runtime package's runtime gets the tools the package contributes when it lists none, and a contract that a data namespace accepts takes its schema from that namespace.
- Dice checks. The narrative reports what it decided (action, attribute, modifier, difficulty and the outcome it wrote); the dice are taken in order and the DC, total and outcome are calculated by the plugin. A wrong outcome is sent back while the turn can still be corrected. Breaking:
check.resolveditems no longer carryroll,dcandtotal. - World generation. A requested content kind that falls short of its target is kept and reported as a warning in place of regenerating the whole world, and a dimension that does not validate is dropped on its own. Breaking:
brief.contentno longer acceptsmemory; sendbrief.contracts: ["memory.blocks@1"]. - Imported characters are
npc-<name>. A blueprint withoutinstantiate.characterIdinstantiatesnpc-<blueprint id>;char-…stays the player's prefix. Recreate development sessions of Emberback and Lantern Barrow. - Text limits fit every language. The guide's recap, decision and prompt limits and the relationship graph's node summary were sized for Chinese and rejected most English calls.
- A rejected tool call says what the tool accepts. The story-event planner lists the event IDs, dimensions and time fields a condition can use; the character form lists every refused field and the attributes the world lets it collect; the clock names its units (
worldTime.value.units); a select default names the option values; a text over a dimension's length limit is told how many characters to remove. - A streamed model call is limited by silence, not by total time. A stream is ended when the model sends no text, no reasoning and no tool call for the idle timeout (
agent.loop.idleTimeoutMs, 120 seconds by default); a model that keeps writing is not cut off, and the time it writes does not count against the runtime'stimeoutMs. The wait for the first output (firstTokenTimeoutMs) is 120 seconds by default, up from 30.callTimeoutMsnow limits only a model call that is not streamed. The gateway's default request budget follows the same rule: output of a stream moves its 120-second deadline, up to 30 minutes; a budget with an explicittimeoutMsordeadlinestays fixed unless it also setsidleTimeoutMs.
Fixed
- Sessions created before dimension values held one language open again. Such a session stored the world's locale maps in its dimension records; every read failed validation, so opening it returned HTTP 500 and the page showed the error again and again. The server now rewrites those records in the session's language the first time the session is used, and keeps the records as they were under
_legacy.dimension-locale-maps. - A number setting can be retyped. Each key press was validated and saved, so a half-typed value outside the range was refused and the old value came back: "World authoring: wait for the model" could not go from 120 to 300 by typing.
- Errors show above an open dialog. Toasts were drawn inside the app root, under every dialog, so a failed save in Settings showed nothing. They are drawn above dialogs now, and using a toast does not close the dialog.
- "Expand turn updates" shows its description, and "Chat window message limit" has one.
- The getting-started guide always closes. The guide stayed open until "finished" was saved. When the settings could not be loaded — a
settings.jsonfrom before the v2 format, for example — every write is refused, so the guide showed "Could not save setting" and no button closed it. It now closes, shows the reason as a toast, and opens again on the next launch. The desktop log records a settings file that could not be read, and logged errors show their message and stack instead of{}. - Generated worlds no longer reuse the prompt's example dimension names. The generation prompt showed one example with the IDs
setting,opening,repairBudgetanddiscoveries, and one numeric resource. The default model copied those IDs into most worlds, whatever the world was about, and wrote one resource where the opening kit asks for two, so those worlds came with the same warning. The example now has placeholder IDs and shows two resources. - AI world generation no longer fails at 120 seconds on a slow model (#122). The generator passed no request budget to the model gateway, so the gateway's default 120-second deadline ended every attempt before the route's 150-second timeout applied. A world that took longer to write failed three times and showed "Generation failed" after six minutes, and no setting changed that. World revision and translation had the same limit.
- "Edit llm.toml" works on a fresh desktop install. The button failed with
"llm.toml" is not availableuntil the file had been created by hand, and the error appeared where it was easy to miss. The first click now createsllm.tomlfrom the built-in default and opens it; failures show as a toast. - Settings no longer attributes the built-in default model to a file that does not exist. Providers & models tagged the built-in DeepSeek default "From llm.toml" even with no
llm.tomlon disk. It is now tagged "Built-in default", and providers that come from the file or the built-in default explain why they have no delete button and how to remove them. - Local builds ship a production web client. With
NODE_ENV=developmentin the repo-root.env(the value in.env.example),pnpm buildandpnpm build:electronbundled a development-mode web client: the router devtools were visible and React ran its development build. The web build now defaults to production; aNODE_ENVexported in the shell still takes precedence. CI and released installers were not affected. - A session no longer mixes languages. An English session of a bilingual world sent about 3,400 Chinese characters to the model each turn (tool definitions, world name and summary, dimension labels, plugin data stored in two languages, the default calendar), and the fact-extraction agent was never told the session's language because its context hook dropped the framework's opening. All of these are corrected, and a system prompt that a hook replaces keeps the language directive.
- Imported world rules reach the prompt with their titles (
[World Rule: The Party Is Alive]), where the model read the rule's ID or its first trigger word. - Field names that are language codes are not taken for translations.
{ id: "torn-letter" }was read as an Indonesian locale map and replaced by its value. - Fewer rejected tool calls with small models. Array arguments sent as JSON text are explained when they do not parse, and closing brackets after a complete value are dropped; an inventory change lists its item when the model named it only there; the dimension tracker accepts a slash path, a
reasonon a change, and one dimension split over two entries; a zero duration is accepted in any time unit;emit-eventsays that the event is recorded and must not be sent again. - A turn's memory is not lost to one unreadable reply. Memory extraction asks the model once more when the reply is not the JSON it asked for. A job reads only its own turn, so a failed job dropped that turn's facts.
pnpm create-pluginscaffolds follow the current prompt and label contracts (English manifests and prompts, Chinese labels inlocales/zh.yaml).- A slow model is no longer cut off in the middle of a turn. Every model call of an agent runtime had a fixed 120-second budget, whatever the runtime declared, so a narrator with
callTimeoutMs: 120000andtimeoutMs: 240000was ended at 120 seconds with half its text shown, and a retry after a silent first attempt had no time left to run. One model call may now use the runtime's remaining time, and a stream that keeps writing is not ended. pnpm dev:electronreads and saves settings without the dev server. In dev mode the desktop main process sent settings and API keys to the Vite dev server as if it were its own sidecar. Where Vite listens on the IPv6 loopback only, the request to127.0.0.1was refused and each save loggedsettings:save sidecar fallback. Where the request arrived, the dev server, which runs withoutCOVEL_DESKTOP_REST, answered an empty settings bundle and refused the write. Dev mode now reads and writes the files of its home directly, and a window reopened from the Dock loadslocalhost. A logged error also shows its cause, such asECONNREFUSEDbehindfetch failed. Packaged builds are not affected.
Removed
docs/v2/internals/refactoring-plan.md. The current-only convergence it planned is finished; the resulting contract is described inAGENTS.md,docs/v2/internals/flow.mdanddocs/v2/internals/storage.md..ui-stage-cattheme hooks. Stage options use the shared.ui-choicehooks; a theme that styled.ui-stage-cator the earlier card-shaped.ui-choiceshould restyle.ui-choice-index,.ui-choice-contentand.ui-choice-eyebrow.apiCallUI action name. It was never handled by any mount point.- Inline locale maps in world and plugin packages,
PLUGIN.en.md/RUNTIME.en.md, thelocalefield of an action request, theresolveI18noption ofworld-dimension-get, and thememorycontent kind of the world generator. See Changed for what replaces each.
Breaking contracts and upgrade notes
- Plugin prompts: the body of
PLUGIN.md/RUNTIME.mdis read as English and*.zh.mdis the only variant.PLUGIN.en.md,RUNTIME.en.mdand variants for other languages are no longer read. A plugin whose main body is Chinese must move it to*.zh.mdand supply an English body. - Plugin packages:
pnpm validate:pluginrejects a locale map written in a manifest or a UI spec. Write English in the main files and put the translations inlocales/<locale>.yaml. - World packages:
pnpm validate:worldreports a locale map written inworld.yaml, a dimension file or another structured source asinline-locale-map. Keep the default language in the main file and move each other language to<name>.<locale>.<ext>.world-dimension-getno longer takesresolveI18n. - Sessions: the content language is set when the session is created and must be an edition the world has.
POST /api/actionsanswers 400 to a request that carrieslocale. - Sessions created by an earlier version that stored locale maps in their dimension records are rewritten in the session's language the first time they are used. The records as they were stay under
_legacy.dimension-locale-maps. - Recreate development sessions of Emberback (its world data source IDs changed) and of Emberback and Lantern Barrow (imported characters are now
npc-<name>). check.resolveditems no longer carryroll,dcandtotal; the dice plugin calculates them. A third-party consumer of that event must not read them.- World generator:
brief.contentno longer acceptsmemory; sendbrief.contracts: ["memory.blocks@1"].createWorldtakesidleTimeoutMsandonProgress, and itsattemptTimeoutMsoption is removed. - Agent runtimes:
agent.loop.callTimeoutMslimits only a model call that is not streamed. A streamed call ends afteridleTimeoutMsof silence (120 seconds by default), andfirstTokenTimeoutMsis 120 seconds by default, up from 30. - Themes: the
.ui-stage-cathooks are removed; restyle.ui-choice-index,.ui-choice-contentand.ui-choice-eyebrow. TheapiCallUI action name is removed. - Settings: a
settings.json(desktop) orcovel:settingsentry (browser) that this version cannot read is moved aside (settings.v1.bak.json,covel:settings.v1.bak) and the settings start from their defaults. Single values that are no longer accepted go back to their defaults after the file is copied (settings.conflict.bak.json). Settings → Data shows the copies. API keys are not affected. - A browser with no saved appearance opens in the Panel scheme. A saved choice is kept.
pnpm create-plugin <name>scaffolds have no install step and nopnpm lint; check them withpnpm validate:plugin <dir>andpnpm test:runtime.--with-toolsplugins are named@covel/plugin-<name>.- macOS Apple Silicon and Windows x64 artifacts remain unsigned, and macOS artifacts are not notarized. First launch may show Gatekeeper or SmartScreen warnings.
[0.0.45] - 2026-10-03
This release adds hidden story content that unfolds during play, lets a world require capabilities and install together with its plugins, merges the bundled plugins while cutting model calls per turn, and makes turn commits and background jobs follow what was actually saved (#101–#108, #110–#115).
Added
- Worlds can require a capability instead of naming a plugin.
pluginPolicy.requireslists contract IDs such asaction-check@1. The only installed provider is enabled automatically, and any plugin that provides the contract satisfies it. With no provider, or several and none chosen, the prep screen says why and starting is disabled; a provider the player turns off leaves a notice and the game still starts. Lantern Barrow now requiresaction-check@1. The prep screen labels providers of a required contract "Required by world" and the world's other default plugins "World default". - One install for a world and its plugins. Settings → Install & manage has a single GitHub field. A link lists the plugins and worlds found there together; tick what to install and confirm once. The selection installs as a unit, plugins first, and anything already written is removed if a later package fails. The preview reports up front when a world's required contract has no provider among the installed and selected plugins.
- Collections. A
covel-collection.yamllists the worlds and plugins an author ships together, including packages pinned by commit in other repositories.pnpm create-collectionscaffolds one,pnpm validate:collectionruns the installer's completeness check in CI, andpnpm pack:collectionbuilds one ZIP for offline import through the new "Collection package" drop zone. Seedocs/v2/ship/collections.md. - Host version ranges. A plugin's
PLUGIN.mdand a collection manifest acceptcovel: ">=0.0.45 <0.1.0". The installer refuses a plugin outside its range and states both versions. - Hidden world data. worldData sources accept
visibility: hidden. Hidden data imports into the receiving plugin's_hidden.<namespace>and stays out of prompts, LLM data tools, extension handlers, and every public API until a plugin reveals it (#101). - Hidden story events. The new
story-eventsplugin reveals world-authored events when their dimension, world-time, orrevealed(chained) conditions hold, handing narration a one-turn cue. All bundled worlds ship hidden events (#101, #102). - Story planner.
story-eventscan plant hidden follow-up events during play every few turns. Itsplotruntime runs only when theplannersetting is on. Plans travel through thestory-event.plan@1contract;story-events/intakevalidates them and stores accepted events in_hidden.planned, where they fire once. The planner sees the world summary, the hero, and this turn's WorldIR when available. Mistport, Emberback, and Lantern Barrow turn it on throughpluginSettings; Haruka Academy keeps its authored romance routes without it. - Concealed runtimes.
io.concealed: truestrips a runtime's prompts, tool arguments, tool results, and outputs from traces, the live stream, the session view,/turns, and manual RPC responses, keeping only names, status, timing, and usage. - Bounded agent history.
agent.history.maxTurnslimits the shared conversation an agent runtime sees; the bundled bookkeeping agents keep two turns. - Plugin gateway limits and paged history.
ctx.gateway.generateText/generateObjectacceptmaxOutputTokensand report the slot's token limits;ctx.readTurnMessages({ after, limit })pages the turn journal. - Retention settings.
COVEL_AUTO_SNAPSHOT_RETENTION(default 20) bounds automatic snapshots, andCOVEL_TRACE_RETENTION_DAYSoptionally prunes trace events. ctx.logicalTurnfor plugins. Function handlers and plugin tools see the scheduler's turn (committed main-loop player turns plus one), the numberstartTurnandintervalcount in;turnNumberstays the raw player-message count.- Dropped event payloads are reported. Only one payload per topic is delivered at each event fan-out depth; a later one now raises an
event-payload-droppedscheduling hazard that names the topic and the emitting runtime, instead of disappearing silently. pnpm validate:world <dir>. Runs the production world schema and worldData checks that release preflight uses, for world authors and CI.- Plugin log level.
COVEL_PLUGIN_LOG_LEVEL(debug/info/warn/error) sets the lowest levelctx.loggerstores; it defaults toinfoin production anddebugelsewhere.
Changed
- Unknown models get a 256K context window. A model with no known limits (not in
llm.toml, the built-in model data, or the LiteLLM snapshot) now falls back to 262,144 tokens instead of 32,768, for story and plugin calls alike. The small window was a testing aid for compaction; it refused large plugin prompts such as the story planner's (Context budget exceeded … limit 16384). - Model data refreshed. The bundled LiteLLM snapshot is pinned to
5dbe4f95(2026-10-03, 4,459 models), so newer models such asqwen3.8-flashresolve their real limits and prices. A weeklyUpdate model dataworkflow now pins the newest LiteLLM revision and opens a pull request with the regenerated snapshot. - Story-first chat layout. Each turn's read-only plugin cards (codex discoveries, achievements, status changes) fold into one "Turn updates · N" line that previews the first titles, so narration keeps most of the screen. Forms, choices, suggestions, images, and warnings stay open. The new General setting "Expand turn updates" restores the open layout.
- Faster dimension settlement. The tracker's prompt now carries every rule, schema, and frozen value it needs (within a 24000-character budget), it runs without reasoning, and
update-dimensionsacceptschanges: [{path, value}]so large dimensions are patched instead of rewritten. In a Mistport session settlement dropped from 20–100 s to about 5 s per turn. - Faster character tracking. The character roster given to
char-creator/character-trackernow carries each character's current fields (within a 12000-character budget), so most turns settle in one call without aget-characterread. The tracker runs without reasoning, and a stalled call times out after 30 s and retries once instead of failing the runtime after 60 s. - A plugin's own code (function runtimes and its local tools) may now write its own
_hidden.*namespaces. The REST API and the builtinplugin-data-set/plugin-data-set-batchtools reject every_namespace. - Background jobs share one durable queue. plugin-rpc
execution: backgroundruntimes, prompt builders invoked withexpectsBackgroundFollower, and background event followers now run as_runtime_jobson the runtime job worker, queued in the commit that produced them, with the same lease, cancel, and retry routes as detached stages. The in-process queue and the legacy_jobsrows are removed; panels keep the virtual_jobsdata source. A runtime that reports its own failure settles its job as failed while its writes still commit. - Leaner storage. Structured runtime outputs no longer enter the conversation timeline; automatic snapshots keep one row per completed player turn and exclude control-plane namespaces; terminal runtime jobs keep the newest 20 per runtime with their status rows; the write-only
runtime_resultstable is removed and dropped at boot. - Fewer reads per turn. Runtime job listings query one namespace and reconcile incrementally, detached job descriptors carry only declared upstream outputs, committed world-model reads are shared once per execution, and the plugin log ring trims every 20 writes.
- Trigger history lives in a ledger.
maxTriggerCountandcooldownTurnsread a kernel-owned per-runtime ledger written in the commit that ran the runtime, instead of counting runtime rows in the conversation journal. Rolled-back executions do not count, cooldowns survive compaction, and a run with no text, form, or UI block no longer writes an empty journal row. Existing sessions start with an empty ledger.getTurnMessageStatsreturns only the player-message count. - The runtime job worker claims from the woken session. Enqueues, retries, resumes, settle waits and job completions wake the worker for their session; a full scan of every session runs at startup, on a wake without a session, and in the 30 s maintenance pass.
setupRuntimeswriters share one helper that re-reads the session and replaces only the entries each writer owns.- Fewer bundled plugins. Plugins that only ever worked together are now one package:
scene-castisscene-stage'scastruntime,character-presenceis part ofcharacter-blueprint(shown as "Character Profiles"),story-plotterisstory-events'plotruntime, andscene-promptsisguide(shown as "Action Suggestions").director's writing notes are part of the narrator prompt. Capabilities such asscene-cast@1,character-presence@1,story-event.plan@1andscene-prompts@1are unchanged, and world packages need no data changes. Worlds and packs list the merged plugins;activeSpeakerCountmoved topluginSettings.scene-stage. - WorldIR extraction takes about half as long. Its output length sets its latency, and every bookkeeping plugin waits for it. The extraction input names session characters by short word handles (
tomas-reed) instead of UUIDs or session-prefixed ids, which the model repeated in every reference;submit-world-factsmaps them back, so consumers still receive the real ids. The prompt asks for each fact once, lists only newly appearing entities (the tool declares referenced session characters), and keeps appearance and quotations out ofattributes. On qwen3.8-flash in Emberback, output fell from 1,800-2,700 to 1,100-1,400 tokens and the call from 25-43 s to 14-21 s. - The codex records a turn in one call it cannot get wrong.
sync-codex-entriestakes oneentrieslist and matches each entry to a stored one by title: a match adds the new content, merges tags, and only raises the rarity; any other title creates an entry (at most three per turn, extra new titles are skipped). The model no longer copies entry ids or chooses betweenunlocksandupdates; in one Emberback run a guessed id, a fourth new entry, and missing update fields sent the codex back four times in one turn (32 s). New entries are keyed by their title (codex-west-herb-garden) instead of a label plus a 32-character random suffix, so the injected entry list is shorter too. - Prompts carry no result ids.
<runtime-inputs>,<runtime-exports>, and the WorldIR extraction input keep each slot'spluginIdandruntimeIdbut dropresultId, a UUID only tools and the kernel use; tools still read it fromctx.inputSlots. - IDs read as words. Models read IDs in injected data and some write them back, so long random parts cost tokens and invite copy mistakes. Imported characters keep their world ID (
npc-mio, not<sessionId>-npc-mio; character keys are per session already), the player ischar-<name>, and codex entries and relationship-graph nodes take their name as words through the newwordIdhelper, with-2on a clash.shortIdkeeps an 8-hex random part instead of 32. IDs stay within[a-z0-9-], so a name without ASCII letters gets the 8-hex part instead (char-3fa9c1d2). - The narrator writes without looking people up first. Its prompt now carries every non-player character's description and fields through the new
{{ characters.npcs }}template variable (about 8000 characters; past that, names only), and it callsget-characteronly for someone not listed.get-characteralso resolves a partial name ("Mina Park" finds "Dr. Mina Park") and answers a miss with candidate names instead of sending the model tolist-characters. In Emberback the narrator had spent two or three model calls per turn on lookups before writing. - An unchanged dimension no longer fails settlement.
update-dimensionsdrops an entry with novalueand no change (changes: []), the way models mark a dimension that stayed the same, instead of rejecting the whole batch for another model call. - Story prompts carry whole dimension values.
<world-dimensions>shows each value in full while the snapshot fits 12000 characters (it cut every value at 240), cutting the longest values first only when it does not fit. A cut value sent the narrator toworld-dimension-get; in one Emberback turn it read dimensions in four successive model calls before writing. - Dimension settlement skips reads it does not need. When the tracker's prompt holds every rule, schema, and value,
world-initwithholdsworld-dimension-get,world-dimension-list, anddimension-rule-getfrom it, so it settles in one call; they are offered only when the rules block lists truncated dimensions. - Affinity and codex always answer with a tool call (
toolChoice: required), as the other bookkeeping agents do; their no-change path is stillruntime-done. Their conversation ends on the player's message, and the model sometimes continued the story before calling the tool. - Bookkeeping without extra model calls.
inventoryandcore-questnow read fixed WorldIR fields in function runtimes instead of running an agent each, so a turn in Emberback or Lantern Barrow makes two fewer model calls.world-irvalidatesinventory_changeandquest_changeevents, and reads the names the session already tracks through the newworld-ir.vocabulary@1contract, published in pre-turn byinventory/vocabularyandcore-quest/vocabulary. A quest is created only from an explicitacceptedevent. Affinity stays an agent: reading a fixed WorldIR field recorded far fewer changes than the agent's own judgment. - One thinking setting per slot. An
llm.tomlslot takes the same portablereasoningEffortas runtimes and Settings models (for examplereasoningEffort = "disabled"), translated per provider like the AI SDK's top-levelreasoningoption. It is the model default and appears in Settings. - One action-suggestion format.
guidegives every mode a short recap, the decision the player faces, and 3–6 ready-to-send phrases, which stage mode also shows as choices. The safe / aggressive / creative categories are gone. - The e2e plugin harness matches the current pipeline. It finds forms in runtime effects, reads back each request's executions by
turnId, countsruntime.deferredas a run of a detached runtime, checks background jobs endsucceeded, fails uncommitted turns, and gates expectations onstartTurnandinterval. By default every runtime now uses the configured models (--slotonly overrides the story slot), and Phase 6 lists the model each runtime called. - One authorization prompt for a session's community plugins. Entering a session whose community plugins await authorization opens a single prompt that names each plugin with its ID, version and description. All are ticked; untick any to leave it paused. Each plugin still gets its own grant, which lasts for the session until the backend restarts. Before, the prompts came one per plugin and showed only the plugin ID.
- A check plugin can be replaced without editing the narrators.
dice-checknow publishesaction-check@1, which carries onlycheckContext: this turn's check resources, the rules for using them, and the receipt to submit.narratorandchat-mode-narratorno longer state the DC ladder, the natural 20 / natural 1 rule, or thecheck.resolvedemission; they follow the injected block, so a plugin with a different resolution system can provide the same contract. The d20 rules themselves are unchanged. - One agent guide and faster CI.
AGENTS.mdnow holds the rules for every coding agent andCLAUDE.mdimports it; the outbound-request security boundaries are documented indocs/v2/internals/security.md. Web unit tests run in their own parallel CI job, and Turbo evicts cache entries by age and size (cacheMaxAge,cacheMaxSize) instead of keeping them forever.
Fixed
- A world that asks for an uninstalled plugin can start. The prep screen sent the missing ID to session creation, which refused it as an unknown plugin, and the player could not untick a plugin that was not listed.
GET /api/worlds/:id/plugin-plannow returns only installed plugins in itsrequestedlists anddefaultPluginIdsand reports the rest inmissing; the prep screen names them and the session starts without them. - Dimension patches cannot reach a prototype.
update-dimensionsrejects achangespath containing__proto__,constructororprototype. The path comes from model output, and such a path wrote onto the server process'sObject.prototype. - A first bare
runtime-donegets one correction. Acompletion.require: tool-useruntime whose first answer is onlyruntime-doneis now told once that the terminator records nothing, as a bare text answer already was, and fails only if it still calls no business tool. On a quiet opening turn the dimension tracker answeredruntime-done("nothing changed"), failed at once, and left settlement pending, which refused every later turn. - Dropped runtimes leave no conversation rows or trigger counts. A runtime whose writes are dropped next to a committed story no longer appends its text, interaction, or UI cards to the conversation and is not counted for
maxTriggerCount/cooldownTurns, like any other failed run. - A story stuck repeating a tool call still narrates. When loop detection gives up on a
storyruntime, the tools are withdrawn and the model is asked for the narrative instead of failing the runtime, which rolled back the whole player turn. In Mistport, with no dice plugin active, the narrator invented a check and re-sentcheck.resolveduntil the loop guard failed the turn.emit-eventnow also tells the model not to retry a topic no active plugin consumes. - Dimension validation names the failing translation and length. A localized (
x-i18n) value reports the locale in its path and a length error gives the actual length (note.en-US: Maximum length is 240 (got 281)). With onlynote: Maximum length is 240, the tracker resubmitted the same too-long English text until loop detection failed it, leaving settlement pending. - The home page plugin showcase shows real plugins. Five of its six tiles looked up capabilities no plugin lists under
providesand read "Awaiting a plugin to claim this capability". Each tile now uses a provided capability and prefers the plugin that declares itself its default; the stage label comes from the providing runtime instead of retired priority numbers; the grid no longer leaves empty cells; and without a backend the tiles describe the capability. - The e2e harness completes chained setup forms.
e2e:verifysubmits each setup form a request surfaces (up to three), fills number fields from their defaults, and spends apoint-buyform's budget, so Lantern Barrow's point allocation no longer keeps the session in setup. - Narration streams again. The narrator's review hook no longer turns streaming off. A story step whose text will not be final (preparation before tool calls, or a draft the review rejects) now ends with a
narrative.deltacarryingreset: true, and the client clears it before the next step; the reviewed final text still replaces the stream on completion. - One failing plugin no longer discards the story. When a turn's narrative succeeds, each optional runtime commits in its own savepoint, so a rejected proposal drops only that runtime's writes.
PreStateCommithooks run before the commit transaction opens. - Suspended turns keep their detached jobs. Jobs a suspended execution would have queued are released when it resumes.
- Settlement events are recorded once. A pending dimension settlement no longer fails to persist on the events table's primary key every narrative turn.
- Hidden data stays off the live stream.
plugin-data.changedevents carry the same public projection as the REST reads:_hidden.*changes are not announced, and runtime job and dimension rows omit private fields. - A targeted narrative uses the frozen dimension snapshot when the dimension provider was not scheduled, instead of being skipped.
- Rejected drafts keep their reasoning. When a review rejects a narrator draft, the draft goes back to the model with its
reasoning_content; DeepSeek and DashScope thinking modes rejected the retry with HTTP 400, rolling the turn back. - Prompt-builder jobs say what they are doing again. A job queued with
expectsBackgroundFollowercarriesphase: "prompt", and the image jobs panel shows "Generating image prompt…" and then "image job queued" in the player's language. - The player message stays first after a reload. Reloading a session no longer renders the latest turn's player message below that turn's narration and action suggestions.
- One recovery per reconnect. After a server restart the client no longer runs a second full state reload for the
system.resetthat opens the new stream. - Dimension tables stay readable in a narrow panel. Cells keep a minimum width and the table scrolls, instead of squeezing CJK text to one character per line.
- Forks and old databases drop job rows. Forks no longer copy job or log rows from snapshots taken before those were excluded, and rows of the retired
_jobsnamespace are deleted at boot. - Reasoning from groq and similar hosts is read. The Chat adapter accepts reasoning sent as
reasoningas well asreasoning_content, so a reasoning model no longer hits the first-token timeout while it thinks. A follow-up echoes reasoning under the field its endpoint emits; groq rejectsreasoning_contenton assistant messages. - Symlinked plugin directories load. A plugin directory symlinked into the user plugins dir is discovered like any other, with file reads confined to its resolved root; a dangling symlink is skipped with a warning.
- Rate limits no longer trip the first-token timeout. While the transport waits out a 429 or 5xx
retry-after, the first-token timer pauses and restarts once a later attempt is answered; the call timeout still bounds the wait. On a rate-limited provider the narrator was aborted and retried into the same limit every turn. - Dimension settlement stops slipping into prose.
world-init/dimension-trackernow requires a tool call (toolChoice: required). Its conversation ends on the player's unanswered message, and without the requirement the model continued the story about half the time; once it only read a value and then wrote prose, leaving settlement pending and blocking the next turn. Over 10 DeepSeek turns it now settles on the first call every time. - WorldIR extraction retries far less.
submit-world-factsrepairs mechanical slips before validating instead of sending the call back: it declares referenced session characters missing fromentities, drops copied extraction input, and moves details written beside a fact's fields intoattributes. Each retry cost a whole extra model call, on most turns with weaker models. - An argument sent as JSON text no longer costs a retry. When a model sends an array or object argument as its JSON text,
tool()parses it where the schema expects that structure and validates again, instead of returningVALIDATION_ERRORfor another model round trip. In one Emberback run on qwen3.8-flash this was two of the eight rejected calls (codex and the dimension tracker). - English quests keep their objectives. Advancing a quest whose objectives are in English no longer merges them into one; objectives without Han characters are compared by significant words.
- A guard can skip a runtime with a strict output contract. A guard's
skipflag is no longer validated against the runtime's output contract or bound into consumers; the remaining fields are the output. Before, any guard skip of such a runtime failed withcontract-output-invalid. - Character tracking settles quiet turns.
sync-charactersaccepts an empty batch as "no change", andchar-creator/character-trackernow requires a tool call. Without the requirement the tracker sometimes continued the story instead of recording changes. - Retries need a committed source. A plugin-RPC runtime retry with
retryFromTurnIdnow returns409 retry_source_not_committedwhen that turn rolled back, instead of seeding its uncommitted results so a downstream runtime passes itsneedsgate on writes that never landed. - Detached jobs use their source request's models. The readiness check and the execution of a queued detached job both resolve the model under the request that queued it — slot bindings, custom presets, and plugin
llm.tomltargets included. A job pinned to a plugin model target no longer stays queued while the request holds the matching key, and a worker woken by an unrelated request no longer reads that request's configuration. - Job status follows the save result. When a function runtime reports a job
succeededthroughctx.progressbut its writes do not commit, the job now endsfailedinstead of keeping the success. - Dropped plugin writes no longer read as success or feed later work. When an optional runtime's writes are dropped next to a committed story, finalize also drops every runtime that hard-depends on it (turn-scoped
needs, requiredinputs, event subscribers whose only emitters were dropped), and the turn queues no detached job or background follower derived from it; detached jobs that are still queued see it asfailedin their frozen turn digest and upstream results. A story or setup runtime whose hard upstream was dropped rolls the turn back. Dropped runtimes are settled asfailedin the persisted turn result, their reported jobs endfailed, the retry ledger treats them as failed, and the live stream sendsruntime.failedfor them.FinalizeExecutionOutcome.isolatedRuntimeIdsis replaced byisolatedRuntimes({ runtimeId, error }), andextraInTxreceives the dropped runtime ids.
Removed
unlock-codex-entriesandupdate-codex-entry. They were only internal steps ofsync-codex-entries, which now does both.- Scene background generation.
scene-stageno longer calls an image model; a location without world art shows the world image. ItsmodelPresetId,autoGenerateScenesandmaxGeneratedScenessettings, thevisualHintfield ofscene.set, and thependingflag ofstage.backdrop@1are gone. story-guard, which no world enabled and whose default block list matched no current tool.cost-gate. Its per-session token cap counted in process memory (reset on restart) and charged cached input at full price. Mistport enabled it by default with a 400k soft / 600k hard cap; a turn costs 100k–150k tokens, so the soft cap stopped every post-narrative runtime by turn 6, which also left that turn's dimension settlement pending and refused the next turn. ItsCOST_GATE_SOFT_TOKENS/COST_GATE_HARD_TOKENSvariables are gone with it. Control spend through provider limits and slot model choice instead.
Breaking contracts and upgrade notes
- Imported characters no longer carry a session prefix in their ID. Sessions created earlier keep their prefixed IDs; a world data sync into such a session can add unprefixed copies, so recreate development sessions.
llm.tomlslots no longer acceptreasoning_effortor[covel.<slot>.thinking]; usereasoningEffort, orproviderOptionsfor native fields. Loading fails with a message naming the replacement.- The
update-inventoryandupsert-questsmodel tools are gone. A third-party WorldIR provider must emit the fixedinventory_changeandquest_changefields for those plugins to record anything. - Development sessions that list
scene-cast,character-presence,story-plotter,scene-prompts,director,story-guardorcost-gatemust drop them from their active plugins, or be recreated. Data those plugins wrote under their own ids is not carried over; world data re-imports into the merged plugins on a new session. - The
dice-check@1contract is replaced byaction-check@1, which no longer includes thedicearray. A third-party plugin that provides or consumesdice-check@1must declareaction-check@1and read onlycheckContext. Thedice-checkplugin keeps its ID, and session data is unaffected. GET /api/worlds/:id/plugin-planlists only installed plugins underrequestedanddefaultPluginIds; uninstalled ones are in the newmissingfield, and the plan carriespolicy.requires. Session creation answers400 world_requirement_unmetwhen a contract the world requires has no provider or an unresolved choice between several.- The REST plugin-data API and the builtin
plugin-data-set/plugin-data-set-batchtools reject every namespace that starts with_. A plugin writes its own_hidden.*namespaces from its function runtimes and local tools. - The first boot of this version drops the
runtime_resultstable and deletes rows of the retired_jobsnamespace. Background work is stored as_runtime_jobs; panels keep reading the virtual_jobsdata source. Back up a development database before upgrading if an older build must still read it. - Trigger history starts empty for existing sessions, so
maxTriggerCountandcooldownTurnscount from the upgrade onward. - Kernel API changes for embedders:
FinalizeExecutionOutcome.isolatedRuntimeIdsis replaced byisolatedRuntimes({ runtimeId, error }),setTurnResultCommitStatustakes the dropped runtimes asfailedRuntimes, andgetTurnMessageStatsreturns only the player-message count. scene-stageno longer readsmodelPresetId,autoGenerateScenesormaxGeneratedScenes, andactiveSpeakerCountis now set underpluginSettings.scene-stage.COST_GATE_SOFT_TOKENSandCOST_GATE_HARD_TOKENSare no longer read.- macOS Apple Silicon and Windows x64 artifacts remain unsigned, and macOS artifacts are not notarized. First launch may show Gatekeeper or SmartScreen warnings.
[0.0.44] - 2026-10-01
This release turns world dimensions into author-defined, session-evolving data, rebuilds the bundled worlds around it, and adds Lantern Barrow, a classic tabletop world (#98, #96, #97).
Added
- Dynamic world dimensions. World packages declare any number of dimensions as
{name, description?, schema, initialValue, updateRule?}. Scalars, objects, arrays, and named records share one model. Dimensions with a natural-languageupdateRuleare settled after every narrative byworld-init/dimension-tracker; values are versioned, validated at the commit boundary, and written with batch compare-and-set on every store backend. Worlds without rules make no extra model calls. - Visible settlement and recovery. Each narrative registers a settlement receipt (
pending-settlement,settled,no-change,manual,skipped). A failed settlement is never treated as "no change": after one automatic model retry it blocks the next narrative until the player retries, resolves it manually, or skips it. - Player-facing dimension panel. Session values render from their schema, with field and row editing, version-checked saves, and recovery actions. Schema
titleaccepts localized text, and the newx-enumLabelskeyword gives enum values localized display names while storing stable IDs. - Lantern Barrow. A bilingual (Chinese/English) dungeon crawl built for
tabletop-rulesanddice-check: point-buy character creation, d20 attribute checks, quests, inventory, companion affinity, and live Delve Map, Barrow Alarm, Lantern, and Renown panels.
Changed
- Bundled worlds are rebuilt for their audiences. Haruka Academy gets a visual-novel pack with affinity, heart routes, festival prep, promises, and rumors; Mistport gets a case board, deep-withdrawal stage, faction standing, and key fragments; Emberback gets an expanded English world guide, two new characters, and live storm, power-grid, signal, and convoy panels.
- Dimension reads are frozen per turn.
ctx.world.dimensions, prompt templates, builtinworld-dimension-list/world-dimension-get, and clients read the same versioned snapshot; update rules and initial values stay private. - World sync protects progress. Changing or deleting an adopted definition reports a conflict instead of resetting evolved or player-edited values. Dimensions are no longer duplicated into constant lorebook entries.
Breaking contracts and upgrade notes
dimensionsno longer accepts the old raw format for the nine fixed categories. World packages, external dimension files, worldData sources, generators, and plugin consumers must use the definition format and read versioned.valueentries instead of world-package values orworld.entries. There is no compatibility path or migration; update development worlds and recreate affected development sessions and browser checkpoints.- macOS Apple Silicon and Windows x64 artifacts remain unsigned, and macOS artifacts are not notarized. First launch may show Gatekeeper or SmartScreen warnings.
0.0.43 - 2026-09-30
This release adds native Gemini support, tightens provider request contracts, and makes execution commits and memory indexing more explicit and atomic. It also fixes plugin panel state and aligns local verification with CI (#91, #92, #93).
Added
- Native Gemini text generation. The
google-generative-ai-v1adapter supports streaming, tool calls, structured output, supported image inputs, and model-specific thinking settings. Google defaults to the native endpoint; OpenAI-compatible Google configurations must explicitly selectopenai-chat-v1and the compatible endpoint. Native Google embeddings, media generation, file uploads, and grounding are not included. - Automatic checks before pushing.
pnpm hooks:installinstalls a pre-push hook that verifies each distinct pushed ref tip in a clean temporary checkout: frozen dependency installation,pnpm check, unit tests, and browser test collection. PostgreSQL integration, browser execution, and packaging remain separate release checks.
Changed
- Provider requests have explicit contracts and shared budgets. Effective capabilities drive model controls; typed provider options, request limits, and deadlines apply across retries and fallbacks. Unsupported configuration fails before sending requests, structured output is validated and normalized, and response diagnostics remain available in traces.
- Execution prepares its own commit. Turn and resume APIs return the public result together with an internal commit carrying the admitted schemas, effects, suspension state, and accounting. Consumers commit the complete prepared execution; public results do not expose pending proposals. Hook settings and runtime generations are captured for detached work.
- Library and plugin interfaces are consolidated. Handlers use shared SDK contracts, Store backends use explicit subpath imports, context construction is asynchronous, and world creation returns a normalized manifest separately from package writing. Unused exports and module cycles have been removed.
- Verification uses the same gates locally and in CI. Fallow checks dependency and import hygiene; release builds reuse CI gates, PostgreSQL checks run without task caching, and browser smoke starts one isolated server stack. The framework uses Node.js 26 and pnpm 11.22.
Fixed
- Memory vectors and indexing progress commit atomically. Memory, SQLite, and PostgreSQL stores compare the expected revision and session incarnation before applying vector changes and progress together. Derived index progress is separate from portable plugin data.
- Plugin panels preserve active edits and interaction state. Equivalent array updates no longer replace drafts, numeric object keys remain object keys, action parameters read current state, and stale callbacks respect the current interaction lock.
- Clean-checkout verification resolves generated plugin artifacts correctly. Generated ZIP paths no longer appear as missing source imports when local test output is absent.
- Live player-flow verification starts every claimed round. The three-round acceptance test sends the third action explicitly instead of treating a selected guide draft as an executed turn.
Breaking contracts and upgrade notes
- Update custom hosts and plugins for prepared execution commits, the shared handler SDK, asynchronous context construction, explicit Store backend imports, and the normalized
createWorld/writeWorldPackagesplit. Update the server, Web client, desktop shell, and framework packages together; plugin and world packages remain independently versioned. - Recreate affected development sessions and vector indexes that use the previous vector-progress representation. Checkpoints omit derived index progress and do not migrate it; restored sessions rebuild their own indexes. There is no automatic migration for old development data. Back up affected storage before recreating it; upgrades from versions before v0.0.42 also require the v0.0.42 upgrade procedure.
- macOS Apple Silicon and Windows x64 artifacts remain unsigned, and macOS artifacts are not notarized. First launch may show Gatekeeper or SmartScreen warnings.
0.0.42 - 2026-09-29
This release introduces versioned plugin extension contracts and the kernel-owned World Model, and hardens execution, persistence, and recovery across the server, browser, and desktop shell (#89). It replaces several development-era contracts; follow the v0.0.42 upgrade guide before opening existing development data.
Added
- Versioned plugin extensions. Declared providers can contribute prompt segments, history transforms, world context, compaction, image workflows, and UI slots. The host validates registrations, input and output, ordering, timeouts, and failure policy for single, collect, and pipeline calls. Contract-based
provides,requires,optional, andconflictsdrive dependency admission instead of plugin-specific kernel rules. - World Model and portable world data. The kernel owns session character schemas and records; plugin views are read-only, and character changes use domain proposals. Lorebook entries have world, player, or plugin ownership. World imports can target versioned contracts, and authored time definitions use
world.time-definition@1. - Server-projected stage slots. Backdrops, cast, dialogue, choices, and character visuals are validated and projected on the server. The client consumes public slot data, including recovered state after stream resets, rather than plugin-private namespaces.
- Plugin-owned memory and development reload. Memory extraction uses plugin data and detached settle jobs; versioned extensions supply history transforms and compaction. Development reload can refresh an approved community plugin while in-flight work retains its admitted generation; later executions resolve the updated dependency graph.
Changed
- Package declarations and execution are separate. Root
PLUGIN.mddeclares the package and its contributions once; executable runtimes use explicit runtime manifests, includingRUNTIME.mdin multi-runtime packages. A package with no executable runtime schedules nothing. Static UI can load without importing plugin code. - Runtime results have distinct channels.
outputis the business value,effectsrequests domain writes or events, andcompletioncontrols turn/setup state. Function return objects with fields such aseventsorpluginDataare no longer treated as implicit effects. Validation, bindings, and exports use the exact function value, including scalars andnull. - Detached work has explicit admission and snapshots. Required settle jobs share a bounded wait budget, and timed-out waits do not cancel the job. Structured form submissions are frozen at admission; complete
turn-digest@1snapshots protect resumed and detached work from later input. Pausing or ending a session waits for relevant settle work.
Fixed
- Provider completion cannot publish partial output. Streams require a valid terminal marker, and provider error bodies remain errors even under HTTP 200. Failed or incomplete output cannot commit effects or be joined to a retry; supported empty transient failures can still retry or fall back.
- Vector and media persistence preserve ownership. Vector indexes bind atomically to an embedding-model identity and reject drift, including same-dimension models; restored sessions rebuild derived vector progress. Media blobs publish before metadata, and world imports pin shared content through commit or rollback so another session's bytes remain available.
- Browser and desktop recovery respect current state. Stale session responses and subscriptions cannot replace a newer view; missing workspace mirrors recover from local checkpoints. Disabled-plugin actions are rejected at admission. Plugin HTML runs in nested sandboxed frames. Desktop sidecar recovery cancels stale starts, reconnects to the recovered port, and bounds repeated crashes; proxy settings persist before transport changes.
- Provider keys and world imports keep their contracts. Atomic provider-key patches preserve omitted providers. WorldIR statement IDs map to stable
world-ir-<SHA256(statement.id)>rule IDs withsourceStatementIdprovenance. Image generation respects cancellation and polling deadlines, and deep merge preserves special JSON keys as own data.
Breaking plugin and data contracts
- Update custom loaders, hosts, and plugins to the current package/runtime manifests, declared dependencies, typed extension handlers, and explicit runtime
output/effects/completion.loadPluginDefinition()now exposespackageManifestand runtimemanifests; static UI usesloadPluginUi(). Host extension scopes read data throughreadPluginData(pluginId, namespace); provider handlers use owner-boundctx.pluginData.get/list. Cross-package named runtime references and undeclared runtime contracts are rejected. - Update integrations for structured
ctx.session.lastPlayerInput(ctx.playerMessageremains text), complete detached turn digests, and separatehostState,sessionState, andserverCodeApproved. Loading host code does not grant session authorization. Image roles resolve explicitly; narrative mode requiresscene-stage@1. - Custom settings and desktop key adapters must apply provider patches atomically: a string sets a key,
nulldeletes it, and omission preserves it. Browser localStorage secret writes require Web Locks in a supported HTTPS or localhost context. Reimport old WorldIR rules to obtain the new stable IDs; changing an embedding model requires a fresh development session and index.
Upgrade notes
- There is no automated compatibility layer or migration for old databases, sessions, or schemas. Stop the server and back up the database plus SQLite WAL/SHM files before using a fresh database path or rebuilding development storage against the current schema. The old Lorebook table can fail startup with
no such column: owner; creating a new session alone does not repair that database. Recreate old sessions, memory, snapshots, browser checkpoints, and incompatible world data rather than restoring them into the new contract. Follow the upgrade guide for the reset sequence. - Update the server, Web client, desktop shell, and framework packages together. Plugin and world packages are versioned separately from Covel and must be updated to their matching contracts; installed user copies do not change when their source repositories change. Validate custom plugins with
pnpm validate:plugin <plugin-directory>before activation. - Development reload refreshes directly loaded entry, handler, and guard modules; changes to relative dependency modules require a server restart. Community server code still runs in the host process. Already-claimed memory jobs are not automatically replayed after a process crash.
- macOS Apple Silicon and Windows x64 artifacts remain unsigned, and macOS artifacts are not notarized. First launch may show Gatekeeper or SmartScreen warnings.
0.0.41 - 2026-09-27
This release makes plugin package declarations composable, tightens activation and service lifecycles, and adds tools for developing multiple cooperating plugins (#86).
Added
- Composable plugin packages. Root and child runtime manifests can contribute shared UI, settings, data schemas, projections, commands, events, memory blocks and entry declarations. Consistent duplicates merge; a package with only entry, Hook, UI or data declarations has zero executable runtimes. Runtime execution remains explicit in the root single-file layout or
runtimes/*/PLUGIN.md. - Plugin diagnostics and composition testing.
/pluginsopens a session-aware debugger view of active plugins, registrations and recent bounded service calls;/plugins <plugin-id>filters it. The test-runtime CLI can load multiple plugins together with--with-pluginto exercise their declared interactions. - Isolated HTML panel drafts. A plugin webview can keep bounded temporary
uiStatethroughsetUiState, restoring a draft when its sidebar tab is revisited within the same session view.
Changed
- Tool isolation and activation diagnostics. Tool names are scoped to the registering plugin, so different plugins can use the same local name; framework tool names remain reserved. Activation diagnostics identify registration failures without exposing arbitrary plugin exceptions.
- Plugin resource and service lifetimes. Failed entry initialization and host shutdown cancel
covel.signaland run registeredcovel.onDispose()cleanup. Service calls support an optional localtimeoutMsbudget, preserve parent cancellation and deadlines, and reject cycles or chains beyond eight calls. A local timeout does not cancel the parent runtime.
Fixed
- Development sessions recover after backend restarts. The Vite API proxy now closes interrupted upstream streams so the client can reconcile the saved execution state and offer an explicit retry, instead of waiting indefinitely without a retry prompt.
Breaking plugin contracts
- Declaration-only single-root packages expose
runtimeCount: 0, and package-level UI descriptors omitruntimeId. Custom registry and UI consumers must handle both cases; enabled Hooks and package settings remain available without a synthetic runtime. - Multi-runtime roots cannot contain execution fields, while each child runtime manifest must declare execution. Conflicting package declarations now fail loading; invalid registrations, unknown Hooks and plugin-local name collisions fail activation and roll back the registration batch.
- Custom
PluginAPIhosts must providesignalandonDispose. Service contexts expire when each call settles; implementations must finish dependent work within that lifetime and pass cancellation to asynchronous work.
Upgrade notes
- Update the server, Web client, desktop shell and framework packages together. Validate complete plugin packages with
pnpm validate:plugin <plugin-directory>and resolve manifest or registration errors before activation. This release introduces no database migration. - macOS Apple Silicon and Windows x64 artifacts are unsigned, and macOS artifacts are not notarized. First launch may show Gatekeeper or SmartScreen warnings.
0.0.40 - 2026-09-25
This release adds community plugin and world directories, GitHub installation and reviewed updates, and restores community plugin selections across session creation and backend restarts.
Added
- Install plugins and worlds from GitHub. Settings accepts public repository or package-directory URLs, previews each package and its exact commit, and requires explicit risk confirmation. Repositories containing several packages support separate, successive installations. GitHub requests use the configured network proxy; installation never runs dependency managers or repository build scripts.
- Review package updates before applying them. Installed packages record their repository, directory, commit and content digest. Players can check tracked branches or choose a tag/commit explicitly, review changed files, and stage or cancel an update. Updates apply at backend restart with content revalidation and file recovery; local package edits and world-editor changes block replacement.
- Official resource directories. covel-plugins and covel-worlds provide English-default READMEs, translation links, official examples and separate community listings accepting PRs. Both application READMEs link directly to the directories and explain installation.
Fixed
- Localized runtime manifests inherit before validation. Minimal
PLUGIN.<locale>.mdtranslations retain required fields and execution settings fromPLUGIN.md, avoiding failed world-time advancement in English worlds. Translated prose remains validated, and translations cannot override execution contracts. - Community plugin selections survive creation and restart (#84). Sessions retain the selected plugin IDs while execution remains gated by current authorization. Opening a session requests missing approval; declining keeps a visible paused state and an explicit retry action. A backend restart requires fresh approval without silently discarding the selection.
Changed
- Jev Choice Demo, OpenAI/DashScope image generation and MiMo TTS are distributed through
covel-pluginsinstead of the application bundle. The official packages follow the current manifest, runtime and permission contracts and include standalone authoring examples.
Upgrade notes
- Update the server, Web client, desktop shell and framework packages together. Existing users of the moved plugins should install their official packages from the directory, restart the backend and authorize them in the relevant session. Preserve any customized plugin directories before replacing old copies; independently stored settings, plugin data and media are retained. No database migration is introduced.
- Packages installed manually or without a current GitHub installation receipt must be reinstalled through the GitHub flow to enable update checks. Updates are checked on request, not installed automatically. World updates do not rewrite existing saves or automatically synchronize their world data.
- Directory inclusion is not a security audit. Community code executes in the backend without process sandbox isolation after authorization; worlds can contain third-party prompts and media.
- macOS Apple Silicon and Windows x64 artifacts are unsigned, and macOS artifacts are not notarized. First launch may show Gatekeeper or SmartScreen warnings.
0.0.39 - 2026-09-24
This release hardens security boundaries (token comparison, provider-key gating, plugin-service slot lending), fixes media ingest redirect handling and permissions.http enforcement, makes session plugin activations persist-first, and repairs stage/dialogue display issues.
Security
- Token and provider-key boundaries hardened.
COVEL_DESKTOP_REST_TOKENis compared through constant-timesafeEqual; raw/api/provider-keyslistings require a real desktop deployment (COVEL_DESKTOP_REST, not merelyCOVEL_HOME+ token); boot warns loudly whenCOVEL_BIND_HOSTis non-loopback while session-owner auth is unenforced;apiKey/headersare stripped fromresolveSlotresults lent through plugin service calls, including nested hops; evaluation adapterbaseUrlrewrites are idempotent. - Actions SSE errors are sanitized. Lock timeouts now map to a coded
session_busypayload instead of leaking session ids and lock-pool internals; other errors only include raw messages in dev. - PLUGIN.md entry paths are realpath-contained. A symlinked entry pointing outside the plugin root now fails activation instead of escaping the lexical
path.relativecheck.
Fixed
- Media ingest redirect contract repaired.
fetchWithRetryhonors an explicitredirect:"manual"opt-in (returning the raw 3xx for per-hop revalidation) while remaining fail-closed by default — previously it threw on any 3xx, making the media ingest redirect loop dead code and breakingctx.images.generatefor providers that return redirecting URLs.ctx.media.ingestUrlnow obeys the samepermissions.httpallowlist asctx.utils.fetchWithRetry(community fail-closed; builtin unchanged). - Missing
committedflag treated as uncommitted.execution.completedSSE handling now requirescommitted === truein both branches; a missing/non-boolean field settles the attempt as failed instead of leaving optimistic output lingering.ErrorOccurredPayloadgains an optionalcodefield, andsession_busyrenders through the i18n sentinel in zh-CN/en-US/ru-RU. - Stale job rejections are distinguished. Pre-execution validation failures record
pre-execution-rejected; rejections at/after the running→committing CAS recordcommit-barrier-rejected. The public reason whitelist maps both to the same converged message. - Lorebook prompt positions unified and strict.
PersonaPromptPositionis the canonical shared type;normalizeLorebookCoordinateno longer accepts the undocumentedbefore-memoryalias or silently downgrades unknown values — it warns and applies the documented default. - Session plugin activations are persist-first. All activation mutations go through
applyPersistedActivations: the persist callback establishes durable state first and a rejected persist leaves memory untouched.syncSessionActivationsis reclassified as lock-scoped read-repair; the manual-trigger sync is linearised insidesessionLock.withLock. - Stage dialog waits for the closing click on the last paragraph. A turn's narration used to jump straight from the typewriter to the decision panel (recap + choices) the moment the last paragraph finished revealing on an ended stream, pulling the text out from under the reader. The final paragraph now pauses like every other one and finishes on the player's click; auto-play advances it on the usual dwell timer. An empty trailing paragraph (trailing
\n\nartifact) still resolves directly, and stories present at mount remain treated as already read. - Narrative review no longer flags quoted speech behind an unclosed quote. A story whose final dialogue line was left unclosed (truncated model output) made the perspective check fall back to the raw text and flag pronouns inside properly closed dialogue — e.g. 「我是班长…」 — as a narration violation. The resulting corrective retry could then shrink the committed story to the model's one-character patch (a lone
」). The check now keeps closed dialogue stripped and validates only the unclosed tail as narration, and correction retries are instructed to resend the complete response so a patch fragment cannot replace the whole draft. Sessions that already committed a degenerate story need a new turn to recover.
Changed
SetupCompletionTrackerextracted from the turn executor. The setup-completion state previously scattered across five mutable cells is now owned by one tracker; the late-setup channel runs groups throughapplyHazardPolicylike main groups, and a setup runtime declaringturnCompletion: detachedruns in the foreground with a diagnostic instead of silently deviating.- Runtime loader and dual-authorization gate extracted from bootstrap.
createRuntimeLoader()inbootstrap/runtime-loader.tsownsloadRuntimeFnplus the community server-code + runtime dual-grant check; fail-closed behavior is identical. - Bare NUL bytes replaced in composite keys.
finalize-execution.tsandvector-recall-search.tsnow use the\u0000escape like the other separator sites, sofile(1)/grep/diff tooling no longer treats them as binary.
Upgrade notes
- Update the server, Web client, desktop shell, and framework packages together.
ErrorOccurredPayload.codeis additive;normalizeLorebookCoordinaterejects the undocumentedbefore-memoryalias (usebefore_plugin/after_pluginper the contract). No database migration is introduced. - macOS Apple Silicon and Windows x64 artifacts are unsigned, and macOS artifacts are not notarized. First launch may show Gatekeeper or SmartScreen warnings.
0.0.38 - 2026-09-21
This release adds configurable evaluation models and public plugin services, keeps the Jev recommendation demo optional, and fixes opening character allocation and stage interaction timing.
Added
- Evaluation models support configurable providers and protocols. TypeSafe Jev, OpenRouter decisions, Vercel evaluation, and supported structured-output providers share typed evaluation questions, normalized results, cancellation, and request tracing. Provider endpoints, models, and role bindings remain configurable.
- Plugins expose public services and custom HTML panels. Function runtimes can discover and call declared services through scoped host contracts. Plugin-owned UI can render in sidebars and stage decision areas with validated resources and session/turn context. The Jev choice demo demonstrates this flow without selecting or sending player actions.
Fixed
- Model roles respect provider capabilities. Text, image, and evaluation bindings use consistent capability checks in settings, preparation, session overrides, connection probes, and runtime dispatch. Clearing a role restores its current configured default; evaluation protocol selection is consistent across provider editors.
- DeepSeek Flash tool requests honor thinking mode. The current
deepseek-flashmodel supports reasoning controls without a model-database entry. Default and explicit thinking requests omit incompatible forced tool choices, while non-thinking plugin tasks retain their required tools. - Opening allocation preserves default character creation (#77). Identity/personality creation runs first, allocation appears in the same character-creation turn, and accepted points update the existing player. Worlds without allocatable attributes skip allocation. Restart and retry markers preserve the accepted allocation.
- Stage decisions wait for execution to finish. Quick replies and plugin recommendations stay hidden during the active turn and return with the committed result, preventing actions based on an earlier scene.
- Demo plugins stay disabled by default.
role:demopackages do not enter default selections through broad tags, capabilities, or a world without selection policy. Players and explicit world/pack declarations can still enable them. Jev demo no longer carries the automatic dialogue-mode tag. - The third-party tabletop installation fixture uses Windows-compatible directory junctions for staged dependencies.
Upgrade notes
- Update the server, Web client, desktop shell, and framework packages together. Custom hosts should follow the current evaluation, plugin-service, and UI contracts in
docs/v2/reference/. No database migration is introduced. - The Jev demo requires an explicitly configured evaluation role and manual or world-authored activation; ordinary gameplay does not require it. Recommendation probabilities describe candidate preference, not action success.
- Recreate unfinished development sessions using the previous tabletop-only character form; its form identity and setup markers are not migrated. Completed character data is retained.
- macOS Apple Silicon and Windows x64 artifacts are unsigned, and macOS artifacts are not notarized. First launch may show Gatekeeper or SmartScreen warnings; platform builds do not imply a complete interactive Windows playthrough.
0.0.37 - 2026-09-19
This release adds world-owned clocks and reusable reasoning configurations, repairs character and memory integration, and makes execution, recovery, and browser persistence consistent across framework entry points.
Added
- World-owned time advances with the story. The
world-timeplugin supports custom calendars, eras, day lengths, and coarse phases declared by world packages. Models propose elapsed time; deterministic tools calculate and commit the clock with the story. Narrators receive the authoritative time, a plugin panel displays it, and/timereads the committed value without advancing a turn. Failed execution preserves the previous clock (#75). - Model reasoning configurations can be named and reused. Provider models carry reasoning defaults, roles can override them, and supported protocols receive the corresponding provider parameters. Story and execution views retain normalized reasoning output separately from visible narrative.
Fixed
Character tracking accepts idempotent results and can correct invalid tool arguments. World attribute constraints reach the model, unchanged values are successful no-ops, and bundled agents use the shared step budget instead of a two-step limit (#72).
Memory updates use the current request's model and credentials. Background extraction follows the selected model role, records correlated failures, and exposes degraded updates to the player. Non-schedulable UI/hook plugins no longer show model selectors with no consumer. Committed memory work has durable recovery intent; failed terminal work is not automatically replayed (#74).
Execution has one commit and finalization path. Player, manual, background, recursive, and resumed work preserve operation settings and scoped permissions. Handler and tool writes remain buffered until commit; cancellation, timeout, hook review, suspension, and terminal notifications follow the same lifecycle.
Tools and hooks cannot retain mutable host state. Tool reads see pending proposals through invocation-scoped capabilities; late reads are revoked after completion. Hooks receive owned snapshots and validated result envelopes, while callback failures remain separate from policy decisions.
World and session lifecycles preserve their own data. Preparation lore survives session creation, snapshots, and forks. World deletion coordinates session cleanup and credentials, and late cache responses cannot resurrect deleted or replaced sessions. Browser workspace operations serialize across tabs.
Shutdown drains owned work before closing dependencies. Foreground requests, background work, event delivery, world reloads, and desktop sidecars settle before stores close; failed startup unwinds acquired resources. Desktop staging copies files without modifying workspace or package-cache sources.
SQLite media initialization releases its connection reference on failure. Settings observer exceptions no longer change persistence outcomes or prevent sibling notifications.
Archival indexing preserves existing vectors and content hashes when character or lorebook reads fail, while successful empty reads still remove deleted records.
Plugin author validation checks raw declarations before optional-field normalization; test-runtime background followers use the production execution and commit lifecycle.
Browser initialization and reconnect share snapshot ownership, preserve live messages and state, and deduplicate replayed state-patch history. Plugin-panel data seeds replace deleted namespaces; missing model bindings remain visible until explicitly reset or replaced.
Model bindings preserve local versus server identity through settings, headers, UI and connectivity probes. Same-named targets route independently; malformed or missing local definitions reject before execution. Request overlays stay out of public model catalogs and implicit defaults.
Durable workers continuously recover expired leases, protecting active commits with nonblocking session locks. Maintenance runs at full execution capacity, stops on close, and retains sanitized failure diagnostics.
Durable job HTTP reads and status events share a public projection that excludes frozen inputs and raw provider/handler failures, while preserving result and execution identity.
Session restore and subscription reads share resource ownership. Live plugin-data and suspension changes invalidate stale initial reads; overlapping full and namespace loads re-read without losing unaffected fields. Invalid selected settings-import values reject before either persistence channel starts writing.
Provider settings reject duplicate connection identities and model references before saving or importing, preventing ambiguous model routing. Inline endpoint and model-name edits keep drafts after persistence failure and recognize their normalized saved values.
Detached runtime results and durable job success now commit together. Failed business output rolls back proposals; post-commit notification failures cannot reopen completed work. Lease recovery respects renewed revisions, expired queue heads no longer block later work, and terminal transitions wait for in-flight renewal.
Partial execution history writes preserve other turns across browser tabs. Delayed history pages and subscription refreshes cannot overwrite a newer visit or request; restored history retains reasoning, tool identity and abort reason.
Session creation now persists embedding model identity and lock time consistently across storage backends.
Model profile saves and role bindings now persist as one ordinary-settings mutation. Removing a last model retains the connection and key. Connection-key cleanup waits for a successful configuration save and reports failures separately.
Provider creation retains drafts on persistence failure. Delayed imports cannot overwrite intervening connection edits, newer imports or an unmounted pane.
Changed
- Settings persistence accepts only its current v2 envelope across browser, REST, and desktop IPC. Unsupported development files remain untouched and must be recreated; the separate v1 settings export format is unchanged. Removed unused host helpers and the manifest validator's
--compatbypass. - Removed the unused session
presetIdfield and its metadata mirror from API, browser, store and snapshot contracts. Model routing continues through slots, request overrides andruntimeModelOverrides. Recreate development checkpoints and snapshots that do not satisfy the current contract; no migration or automatic data deletion is provided. - Model settings use only current provider profiles and version 2 provider exports. Removed legacy model/key migrations, read-time rewrites, unused preset-write APIs and navigation aliases. Recreate affected development model configurations and connection keys explicitly. Current server preset bindings remain supported; ambiguous bindings are rejected.
- Model getters no longer attach API keys, and request key routing no longer borrows provider-family keys. Server-managed key markers are never sent as provider credentials.
- Browser vaults and snapshots now use the current data contract only. Removed historical vault data migrations and snapshot field fallbacks that inferred captured state from the live parent. Snapshot storage and checkpoint validation share the payload schema and reject uncaptured summary references.
- Recreate development vaults older than schema v5 and snapshots missing the current required fields. Unsupported vault versions fail before schema changes commit; no migration or automatic data deletion is performed. Current empty captures and optional world-lore selection remain supported.
Upgrade notes
- BREAKING CHANGE: Update the server, Web client, desktop shell, and framework packages together. Browser-private workspaces require Web Locks on HTTPS or localhost. The current development contracts require vault schema v5, complete snapshot v3 payloads, persisted settings v2, and provider-profile export v2; the independent general settings export v1 remains supported. Export important content before upgrading. Unsupported development settings, credentials, caches, and snapshots must be recreated explicitly; this release does not migrate or automatically delete them.
- Plugin authors must use invocation-scoped
context.storereads and proposal-backed writes; factory-timePluginToolkit.storeaccess is removed. Hooks must return a validcontinueorabortenvelope and publish changes through explicit replacements. UI panels must use supported JSON declarations;_componentPathdeclarations are rejected with diagnostics. See the updated plugin, hook, transaction, and settings references. - Custom hosts must use
executeTurn/commitExecution, inspect resumed runtime failure results, and await worker/event-bus shutdown. Model request overlays now use typedslotBindingswith distinctmodelRef/presetIdidentities. Session creation no longer accepts the unusedpresetId; custom session locks must providetryWithLockin the same lock domain. - World-time inference can add one model call per successful story turn. The fallback calendar is a generic world calendar, not Gregorian conversion. Model failures remain visible and leave time unchanged.
- macOS Apple Silicon and Windows x64 packages are unsigned, and macOS packages are not notarized. First launch may show Gatekeeper or SmartScreen warnings. Windows build/install checks do not imply a complete interactive Windows playthrough.
0.0.36 - 2026-09-17
This patch makes model token budgets configurable and consistent across story, plugin, retry and background requests, and identifies the model responsible when a task fails.
Fixed
- Failed tasks show the actual provider, model and model role. Error details retain upstream diagnostics, including failures before dispatch. Tool and output validation errors identify the model that actually responded, including fallback and resumed calls. Players can change the assigned model or its settings and retry unfinished tasks without regenerating completed story.
- Model settings expose context, input and output limits together. Story and plugin roles share the same settings and resolution path. Effective budgets include TOML defaults and explicit overrides; conflicting limits are visible before a request is sent. Manual limits remain available when the model catalog does not match a provider's deployment.
- Automatic budgets use a 16,384-token output target and a conservative 32,768-token context when capabilities are unknown. Known model limits and the available context can reduce that output target. Explicit output budgets can be raised for complex or reasoning calls within the configured model limits.
- Every request resolves the budget of its actual target. Plugin overrides, hook-selected models, fallback targets, resumed work and background memory calls apply their own context and output limits. Settings previews and sidebar connection tests use the same effective model configuration.
- Provider failures retain useful attribution without exposing credentials. Connection tests follow the normal provider-header and key handling policy, and request-scoped background queues keep settings isolated between requests.
Upgrade notes
- Update the server and bundled Web client together. Existing model assignments, TOML settings and saved overrides remain supported; no SQL schema migration is required.
- A 16,384-token automatic output target is not a mandatory minimum: smaller model limits and explicit lower settings still apply. Configure the deployment's actual limits when they differ from the catalog, then retry failed work.
- macOS Apple Silicon and Windows x64 artifacts are unsigned, and macOS artifacts are not notarized. Gatekeeper or Windows SmartScreen may warn on first launch; updates use manual download and installation.
0.0.35 - 2026-09-17
This release fixes plugin installation, restart and recovery failures, protects existing plugin data, and adds optional tabletop character creation and configurable narrative perspective.
Added
- Optional tabletop rules use the public plugin interfaces. The
tabletop-rulespackage provides integer point-buy creation, server-side budget validation and explicit d20 checks. Worlds configure rules through world data; recorded checks survive retries and restarts without rerolling. A renamed community ZIP exercises the same installation, approval, creation and uninstall path (#62). - Both narrative plugins offer first-, second- and third-person narration.
narrativePersonreuses plugin settings and world defaults, with second person unchanged as the default. Dialogue retains each speaker's pronouns; changes affect future narration (#63). - Third-party function runtimes can call declared tools and validate forms. Public tool calls retain permission, argument validation and transaction boundaries. Typed forms preserve numeric values, rejected forms remain editable, and
fallbackForreplaces a default runtime by capability without disabling its package's other functions. - Plugins can review model responses before tool dispatch or story commit. Public hooks can buffer a response and request up to two bounded corrections. Rejected drafts do not execute tools or become committed story.
- Documentation now includes a gradual v2 reading path. Current guides are aligned with response review, transactional tools, typed forms, graph retrieval and local-first release checks. The v2 entry organizes play, world creation, plugin authoring and validation, with a development-agent guide; full chapter migration continues in later versions.
Fixed
- World art keeps its authored identity and scene continuity. Five Haruka Academy night backgrounds retain their daytime layout, and four Mistport portraits correct fog-rot, expedition lighting and atmospheric detail. Media hashes and localized references are synchronized; Emberback retains its distinct industrial science-fiction direction. Resource checks cover dimensions, transparency and registry references.
- Desktop restart navigates to the new server port. The main process owns navigation after the sidecar is ready, concurrent restart requests share one operation, and destroyed windows release pending state timers (#65).
- Repeated Windows ZIP imports report an existing target correctly. A rename permission error maps to HTTP 409 only when the target exists; genuine permission failures remain errors and existing packages are preserved (#64).
- New entity IDs no longer restart an in-memory counter. Randomized IDs prevent collisions caused by CJK names, normalized labels and process restarts. Graph creation also rejects occupied keys while preserving old IDs and edge endpoints (#70).
- Tool tasks cannot report success from prose or fake tool-call JSON. Guide and scene prompts request their finishing tools. State extractors require an actual finishing call or an explicit no-change call; unresolved tool failures remain failures. Relationship extraction requests a real tool call while allowing either a write or no change (#67).
- Retry results remain attached to the story they supplement. Stage choices stay visible after single or batch retries, including restored legacy records with retained retry metadata. Retries retain declared upstream inputs and guard-provided schemas without regenerating completed story or counting another player turn (#68).
- Narrators can read non-active character profiles. Both engines declare session-scoped character lookup tools, active cast includes structured attributes, and character tracking cannot overwrite an existing character's authored name, type or description (#69).
- Community plugin approvals cover the complete interaction flow. Automatic runtimes request grants before execution; restored forms can reauthorize their source after restart. Multi-plugin validation, denial and navigation retain editable input and do not commit partial submissions.
- Plugin forms and panels refresh after committed changes. Manual forms persist and appear without a reload, activation waits for server confirmation, and long mobile tab lists keep the selected panel visible. Enabling tabletop rules during play initializes rules without recreating the player and passes same-turn rules to the check runtime.
- Character creation and output validation preserve meaningful failures. String attributes accept string-valued suggestions without weakening numeric or enum constraints. Preparation chatter and failed tool result objects cannot turn a failed form operation into empty success.
- The landing page and desktop bridge display the installed package version. Translated headings and preload no longer fall back to stale hardcoded versions. Release validation checks framework versions while allowing independently versioned plugins and worlds.
Upgrade notes
- Update the server and bundled Web client together. No SQL schema migration is required. Existing IDs remain valid; data already overwritten by an older version requires an intact backup or snapshot to recover.
- Tabletop rules are optional. Advanced combat, progression, multiplayer hosting and a live remaining-points counter are not included. Third-party packages can implement their own rules through the documented interfaces.
- Narration is displayed as a complete response after its text check and may require up to two correction calls. This is a limited perspective check, not a guarantee of factual accuracy; it does not clean previously incorrect history or resolve every model inference or relationship-classification error.
- Community runtime grants may need approval again after a server restart. Packaged macOS Apple Silicon and Windows x64 artifacts remain unsigned; updates use manual download and installation.
0.0.34 - 2026-09-15
This patch release preserves saved state through recovery, world-package changes and failed configuration writes, and fixes cancellation and resource cleanup across the client and providers.
Fixed
- Snapshots and forks keep the state captured at their commit. Forks preserve display messages, structured-state schemas and runtime exports without borrowing newer results from the source session. Snapshot export queries respect their session, runtime and revision boundaries.
- Browser workspace recovery completes before newer mutations. Durable pending commits survive failures, missing transient server sessions can be rebuilt, and checkpoint exports retain interrupted runtime results even without a completed turn record.
- Stopping a turn cancels pre-commit hooks and rolls back pending proposals. Already committed snapshots and post-commit cleanup remain valid when cancellation arrives late. Recovery keeps interrupted and failed work distinguishable from completed story.
- World loading preserves the active package and its last complete state. Startup and hot reload share root priority, reject ambiguous IDs and linked manifests, and retain stored worlds when inventories or dimensions cannot be read completely.
- World deletion and installation preserve existing data on failure. File deletion follows the world's storage binding and manifest ID, restores the package after database failure, and refuses missing or ambiguous bindings. Uploads and generated worlds use create-only activation; failed activation cleans only the newly created package.
- Generated and imported packages become visible only after validation. Generated packages and desktop directory imports use staging, hidden directories are excluded from discovery, and imports reject linked manifests and copies into their own source tree.
- World dimensions agree across memory, SQLite, PostgreSQL and API responses. Replacements and explicit removal update the top-level projection, while ordinary edits preserve source and storage ownership.
- Configuration failures no longer discard effective settings or removed keys. Model overlays survive reload boundaries, legacy bindings normalize consistently, default model roles remain editable, and desktop key deletion updates both persisted and running configuration. First launch consistently uses the user's model configuration path.
- Plugin activation and relationship updates follow their current contracts. Pending server entries remain available for authorized lazy activation. Relationship edits refresh labels and changed endpoints while retaining layout; expired edges are excluded even at turn zero.
- HTTP retries and event streams stop promptly on cancellation. Discarded response bodies are cancelled, long Retry-After waits avoid timer overflow, custom abort reasons remain cancellations, and late responses cannot reopen a closed subscription. SSE parsing handles mixed line endings and incomplete final frames.
- Invalid JSON requests fail before dispatch. Object endpoints reject null and scalar bodies; approval scope, event payload and provider-ping fields are validated before consuming approvals or publishing events.
Performance
- Browser commit replay metadata stores fixed-length checkpoint digests instead of repeated full JSON strings. Completed per-session queue entries are released.
- Repeated provider request bodies share one trace entry while retaining each attempt's timing and status. Relationship highlighting uses cached neighbor membership, and SSE parsing slices complete text lines.
Upgrade notes
- Update the server and bundled Web client together. No SQL schema migration is required. BrowserVault upgrades its local IndexedDB metadata to schema v4 atomically; export important saves and custom content before upgrading.
- Custom trace consumers should support
providerRequestsschema v2bodyRefentries as documented in the protocol reference. Historical full-body entries remain readable. - File/database deletion recovery handles failures within the running process; it is not a crash-atomic transaction across both systems.
- macOS Apple Silicon and Windows x64 installers remain unsigned. Gatekeeper or SmartScreen may warn on first launch; updates provide a notification and manual download, without automatic installation.
0.0.33 - 2026-09-12
This release improves agent execution and cancellation, brings onboarding and configuration in line with the current runtime, and makes world editing, session navigation, and portrait replacement more reliable.
Added
- The debugger exposes the model requests actually sent to providers. Agent traces include protocol JSON, effective generation parameters, target fallback and transport attempts, alongside the logical prompt. Authorization headers and arbitrary provider metadata are excluded; redacted resource URLs and omitted fields are marked explicitly.
- Relationship retrieval can use the current scene's cast. When the player names no graph node, an optional same-execution input supplies scene speakers for unambiguous name/alias matching. Retrieval follows only current relationships and adds no model or embedding calls.
Changed
- The getting-started guide follows the current configuration and play flow. New players use the same provider/model editor and model-role assignments as Settings, can reuse existing configuration, and learn about world selection, play packs and opening choices. The guide can be skipped and reopened from world selection.
- Settings and plugin controls reflect discovered runtime contracts. Role assignment and generation panes include configured and plugin-declared slots, multi-runtime plugins expose their individual model overrides, and provider-slot choices retain the active selection. Legacy settings links resolve to the current panes.
- Play and debug views distinguish current work from historical attempts. Execution stages, cancellation and recovery are presented consistently. The relationship view supports clear selection and inspection while retaining the graph's saved layout.
- Plugin activation publishes registrations as one batch. Tools, hooks, RPC handlers and media wires become available only after every entry factory succeeds. Failed activation remains retryable, and rollback removes only registrations owned by that attempt.
Fixed
- Local desktop builds no longer bundle the developer's model configuration. Staging and installer verification reject private server configuration files; startup checks use temporary synthetic settings and also cover a fresh installation without configuration.
- Partial model capability settings no longer break the configuration screen. Schema defaults and normalized values are applied on hydration, writes, imports and synchronization. Invalid import entries are isolated without discarding valid selected settings, and failed writes restore normalized persisted values. This includes the fix for #60.
- Stopping a turn cancels queued model work and prevents an uncommitted turn from being saved. Cancelled waiters leave the concurrency queue promptly, hooks receive cancellation signals, and finalization checks cancellation before committing. Explicit retry remains available for an unfinished player action; refresh does not silently restart provider work.
- Hooks and event guidance reach the actual execution path. The turn executor receives the configured hook pipeline, advertised events include their payload schemas, and the character tracker reserves its final bounded step for a write or an explicit no-op.
- Browser-owned world edits and deletion use the correct storage path. Edits validate dimensions and survive later checkpoint writes; deletion atomically clears related browser checkpoints and pending commits, with best-effort cleanup of owned session mirrors. Deleting a private copy does not delete the shared server world.
- Failed session startup still rolls back after navigation. A late startup failure cleans up its partially created session without changing the world or session the player has since opened.
- Panel navigation survives route changes and delayed rendering. Plugin and image requests wait for session restoration, mobile drawers and plugin UI specifications. Session switches reset view-local state, and stale debugger responses cannot overwrite a newer visit.
- Portrait replacement completes the workspace, approval and persistence flow. Uploads follow workspace restoration, approval retries reuse the uploaded media, and concurrent replacement is blocked. Runtime failures remain visible even in successful HTTP responses; failed slash commands retain their input, and image downloads release temporary object URLs.
- Data and appearance controls handle malformed input and failed operations. Settings export, import and reset failures stay visible, invalid saved themes are filtered before rendering, and provider connectivity controls remain usable within the current settings layout.
- Development configuration is shared across entry points. The API prefers the root
llm.tomlover home defaults, Vite reads root environment files, and proxy settings and web build cache inputs follow the effective configuration. Explicit process environment values retain precedence. - Docker preserves installed and generated user resources. The
appdatavolume backs user worlds/plugins, the model configuration remains a read-only host mount, and health checks use IPv4 loopback. World installation activates the package before returning success and removes a newly created package if activation fails. - Configured resource paths are honored consistently. AI file generation, plugin scaffolding, and runtime tests use the configured user directories; opening
llm.tomlfollows its effective override. Existing world dimensions can reload on Linux through Node's recursive file watcher. - Generated function runtimes use the current handler contract. Default and custom scaffolds return
HandlerResult; authoring examples now distinguish success effects, buffered data writes, and live progress. Regression cases exercise all three scaffold modes. - Media generation supports existing outputs as input. Video, poster, and GIF conversions finish in temporary files before replacing assets. Conversion failures preserve existing assets, and missing explicit source paths fail without selecting another recording.
- Environment switches and maintenance commands match their implementations.
VECTOR_BACKEND=nonedisables automatic semantic-memory work, PostgreSQL preflight follows the configured URL/port, and image scripts share TOML/key resolution. The nonfunctionaldb:migratecommand and unused environment switches were removed; database upgrades remain an explicit maintenance task.
Upgrade notes
- Update the server and bundled Web client together. Existing worlds, saves and model settings are retained; no automatic data migration is introduced in this release.
- Custom plugin entry factories must await initialization and register capabilities before returning. Hooks should honor
ctx.signalfor cooperative cancellation; the framework cannot forcibly stop arbitrary in-process plugin code. Multi-runtime portrait panels should declareruntimeIdexplicitly. - Debug request traces can contain prompts, story text and tool arguments. They use the existing session access and retention boundaries, require care when sharing, and increase trace storage size. The recorded JSON is a diagnostic projection, not an authenticated replay artifact.
- macOS Apple Silicon and Windows x64 binaries remain unsigned; first launch may trigger Gatekeeper or SmartScreen. Updates provide a version notification and manual download, without automatic installation. Back up custom content before upgrading.
0.0.32 - 2026-09-06
This release improves theme rendering and appearance controls, strengthens session isolation and asynchronous recovery, and corrects configuration examples and release documentation.
Changed
- Aurora keeps its flowing gradient with less repeated painting. The theme rotates a reusable gradient layer, strengthens the cyan and violet colours, and lets them show through the reading surface. World art fades into that surface, and reduced-motion preferences remain supported.
- Appearance controls offer independent story colours and a live reading preview. Light and dark modes keep separate text colours; colour swatches and saved theme snapshots preserve the displayed values, including translucent session surfaces.
- Concurrent audit flushes share one drain barrier. Queue draining retains its best-effort persistence semantics and now has explicit coverage for overlapping flushes, newly queued events, and failed saves.
Fixed
- Media uploads recheck ownership inside the session mutation lock. Uploads cannot attach to a replaced session or a session whose owner changed after the initial authorization check.
- Deleting a browser-private session releases its full commit replay cache. Failed deletions retain retry state, and other sessions keep their existing replay history.
- Late plugin operations stay scoped to their original session visit. Errors and approval decisions no longer change another session's plugin display, including leaving and reopening the same session.
- Pending appearance edits stay with the colour scheme in which they began. Switching schemes or leaving a control saves its final value without leaking a preview into the next scheme.
- Saved themes retain their gradient layers, animations and conditional styling. Derived themes preserve scoped CSS through reload and export/import, isolate animation names, and replace their generated token snapshot when saved again.
- Old asynchronous session work cannot overwrite a newer visit. Navigation and creation use visit-scoped cancellation alongside action recovery, including leaving and reopening the same session. Submitted forms merge atomically in browser storage instead of overwriting concurrent submissions.
- Provider event streams handle legal SSE framing and cancellation. Parsing supports mixed line endings and multiple data lines, and stops the reader when completion or consumer cancellation ends the stream.
- Missing event payload references trigger resynchronization. Replay state resets when a transported event cannot be recovered, preventing clients from silently continuing with missing updates.
- Data writes preserve boundary values and rollback state. Plugin data keys containing NUL characters remain distinct, and a rolled-back session deletion restores its embedding target as well as its vectors.
- Media and archive handling preserve their security boundaries. SVG responses retain sandbox and download headers on conditional requests, and a single-file ZIP no longer loses its filename to root-directory detection. Browser checkpoint restoration cannot create an unknown world by omitting its world payload.
- Transitive URI and XML parser dependencies receive patch updates. The workspace pins
fast-uri3.1.6 and@xmldom/xmldom0.8.15 for their existing consumers. - Configuration examples and documentation match current behavior. Image slots use
providerRequestMetadata.imageWire; guides clarify production MemoryStore authorization, local key and world storage, supported locales, theme overrides, unsigned releases, and desktop update notifications.
Upgrade notes
- If your image slot still uses
imageApi, replace it withproviderRequestMetadata = { imageWire = "dashscope-wan" }for DashScope WAN. OpenAI-compatible image generation defaults toopenai-images; seellm.toml.example. - Production MemoryStore deployments enforce session owner tokens and require an operator token for shared-world writes even with
DEPLOYMENT_TIER=self. Follow the API authorization contract and update the server and bundled Web client together. - macOS Apple Silicon and Windows x64 binaries are unsigned; macOS Gatekeeper or Windows SmartScreen may warn on first launch. Desktop updates provide a version notification and manual download, without automatic installation. Back up custom content before upgrading.
0.0.31 - 2026-09-05
This release makes story completion and session recovery more reliable, improves the complete configuration and gameplay flow, unifies API contracts, and hardens browser checkpoints and shared-resource access.
Fixed
- A completed player action must contain a story. A narrator that finishes its tools without prose gets a bounded chance to produce the missing text; an empty or failed story cannot commit pending state changes or advance the player-turn counter. Optional state-extraction failures remain visible without discarding a valid story.
- Character creation preserves numeric attributes. Opening forms only collect schema-compatible text and enum fields, while numeric and compound attributes retain their defaults. Character writes reject invalid types and ranges before producing proposals; rejected form submissions remain editable instead of becoming free-form story input.
- World-fact extraction uses a smaller, task-specific request. WorldIR consumes the current narrative and canonical character identities, defaults to disabling optional reasoning, and requests its submission tool through each provider's native protocol. Explicit user reasoning settings take precedence, and the existing execution deadline remains in force.
- Reconnects recover missing story messages as well as plugin state. Recovery preserves live streams and loaded history, restores committed prose, and keeps each turn's story ahead of its derived guidance and status cards.
- Refresh and transport loss track the original execution. The player and debugger distinguish running, completed, failed, and interrupted work. Refresh only observes the existing action; an interrupted action offers an explicit retry with duplicate protection, and opening recovery does not advance the player-turn counter.
- Failed tasks can be retried without regenerating the story. Single-task and selected-failure retries reuse committed upstream results, preserve successful tasks, and report each recovery attempt back on its original turn. A batch shares one commit boundary; uncommitted results never appear as repaired, and refresh preserves the pending recovery scope. Historical interruptions stay in chronological, expandable records rather than appearing as current progress.
- Character tracking can finish its permitted read-and-submit workflow. Character lookup is directly available within the existing two-step budget, removing a tool-discovery step that could exhaust the budget before the tracker submitted its result. Output failures, interrupted history, task retry, and recovery of uncommitted actions have distinct explanations. The misleading whole-turn retry control has been removed; completed turns offer scoped task recovery.
- Settings persist correctly under concurrent edits and failures. Disjoint changes can rebase under revision checks, conflicting edits retain their drafts, queued identical writes cannot report false success, and mutable object references cannot bypass persistence. Secret entries remain outside ordinary settings import, export, and synchronization.
- Responsive sessions remain usable across viewport changes. Sidebars no longer query panel constraints while registration changes; settings use separate provider-list and detail views on phones, queued choices share the main composer, and debug refresh updates traces, session data, and the session list together. Invalid dice reports and failed runtime updates are surfaced explicitly.
- Browser checkpoints and media access enforce ownership and lifecycle boundaries. Restores validate checkpoint contents and session identity, scoped media access follows the current session lifecycle, and shared-world mutations require operator access.
Changed
- API responses and plugin controls use unified contracts. Server and Web consumers now share validated response envelopes, resource routes, pagination, discovery, selection, and plugin-control schemas instead of maintaining parallel legacy shapes.
- Story and stage presentation are clearer. Theme-aware prose has improved contrast, type size, line spacing, and paragraph spacing. Guidance shows one primary suggestion per category with expandable alternatives. Stage dialogue uses explicit per-turn paragraph identities instead of actor focus; mobile choices scroll above a fixed composer and character portraits remain visible above them.
- Plugin authors can declare bounded extraction defaults and inspect typed inputs. Runtime LLM preferences, read-only hook/tool input slots, suspension restoration, and stage dialogue attribution are documented alongside their generated schemas and regression tests.
Upgrade notes
- External API consumers and custom integrations must follow the updated API reference and plugin contracts; legacy request and response assumptions may require changes. Update the server and bundled Web client together.
- Existing saves are retained. Invalid character values already written by older versions are not silently migrated, and an already accepted invalid character form retains its audit record; start a new character session to use the corrected creation flow. Model-generated facts can still contain semantic mistakes despite schema validation.
- macOS Apple Silicon and Windows x64 binaries are unsigned. Review the platform's first-launch guidance and back up custom content before upgrading.
0.0.30 - 2026-09-03
This release moves eligible media follow-up work into durable background jobs, exposes effective runtime model routing in the UI, and converts structured plugin agents to bounded atomic Function Calling workflows for faster, more reliable world and story updates.
Added
- Safe post-turn function runtimes can finish beyond the foreground turn barrier. Plugin manifests can opt in with
turnCompletion.mode: detachedplus queue/execution deadlines, serial overlap, and stale-result rejection. The scheduler freezes source-turn inputs, falls back to foreground execution with diagnostics when a declaration is unsafe, and validates actual effects again before commit.mimo-tts/auto-narrateis the first bundled runtime using this path. - Scheduler-detached work has durable lifecycle APIs and events.
_runtime_jobsstores CAS-leased jobs on the active DataStore backend while snapshots and browser checkpoints omit control-plane rows; clients can list, cancel, or explicitly retry jobs and receiveruntime.deferredplus origin-turn-awarejob-status.updatedevents. - Plugin surfaces explain runtime behavior with contract-derived feature badges. Session setup, plugin details, and the execution-flow preview expose trigger, LLM/function type, background execution, output role, and declared capabilities without branching on bundled plugin ids.
Changed
- A player turn no longer waits for eligible media follow-up work. Queued job creation commits atomically with the source turn, while background execution runs outside the main session lock and rechecks session/plugin incarnation, version, lease ownership, and declared effects before committing. Restart recovery resumes unclaimed queued work, but terminalizes expired queues and expired in-flight leases without replaying potentially billable provider calls.
- Bundled image prompt generation is now manifest-declared background work. Both OpenAI-compatible and DashScope image prompt agents return a job immediately for every RPC caller, and a background entry now preserves its emitted background-follower chain instead of dropping the provider render. Decision guidance such as
scene-promptsremains inside the foreground barrier because the player needs it before choosing the next action. - Bundled structured agents now submit results through bounded Function Calling workflows. World setup, WorldIR extraction, character tracking, codex updates, relationship extraction, guidance, scene prompts, quests, affinity, inventory, and image prompts use one atomic batch tool where practical, stop immediately after a successful terminal tool, and cap provider attempts so a stalled plugin cannot consume repeated full runtime budgets. Prose narrators remain streaming text runtimes.
- Runtime model selectors now distinguish routing declarations from effective targets. Session Prep and in-session plugin controls keep
runtimeId → slotoverrides scoped to the session while showing the provider/model currently resolved for each slot. Function-runtimemodelPresetIdprovider slots remain separate device-level plugin settings.
Fixed
- WorldIR factual events no longer become malformed domain events. Output normalization only creates
event.emitproposals from{ topic, data? }envelopes whosetopicis a string, so WorldIR's ownevents[]records cannot produce repeatedevent.emit: topic must be a non-empty stringfailures. - Atomic submission tools prevent partial structured-plugin writes.
initialize-world,sync-characters, andsync-codex-entriesreturn their accumulated proposals only after the full batch succeeds; image prompt tools own their fixed event topics instead of asking the model to construct fragile event envelopes.
0.0.29 - 2026-09-02
This release adds Russian across the application, makes source-distributed Web locales catalog-driven, improves AI world-generation recovery, refreshes responsive and themed interfaces, and lets packaged desktop apps notify players about newer releases.
Added
- Russian is now a first-class application locale. The Web catalog, language selectors, settings, browser detection, Electron-native messages, compactor prompts, plugin/world content resolution, and locale-aware validation all support
ru-RUwith an explicit English fallback. - Web locale integration is catalog-driven. A complete canonical BCP 47 JSON catalog can join a source build without editing framework unions or switcher components; the shared locale registry supplies labels, aliases, script-safe matching, and ordered fallbacks, while new tooling scaffolds and validates contributed catalogs.
- Packaged desktop apps now notify players when a newer stable Covel release is available. The startup check uses the configured direct, system, HTTP(S), or SOCKS5 proxy route; players can open GitHub Releases or ignore that version without enabling automatic installation.
Changed
- Responsive and themed interfaces are more consistent across the complete player flow. Landing, world selection and editing, session preparation, gameplay panels, settings, and debugger surfaces now adapt more reliably to narrow screens, keyboard navigation, long localized text, and custom theme tokens.
- Localized framework and package content follows one resolver contract. UI fields, plugin manifests, world variants, prompts, memory summaries, and runtime-generated labels use canonical locale matching with script-safe language variants and English fallback instead of Chinese/English branches.
Fixed
- AI-created worlds no longer reject ordinary lore vocabulary such as
framework,model,cost, orapi. Validation now targets explicit generation-meta phrases, and a package whose lore alone needs cleanup repairsWORLD.mdwithout discarding its already-valid manifest and supplemental content (#50). - Locale switching remains stable across reloads and more than two languages. Settings validation, browser defaults, header and onboarding selectors, and lazy catalog loading all derive from the active Web locale registry (#51).
0.0.28 - 2026-08-31
This release turns the stage view, plugin UI, world data, and runtime context into one extensible interaction system: the same contextual plugin action can be discovered as a slash command, rendered as JSON UI, traced, and committed through a shared pipeline.
Added
- The stage view has a visual-novel direction system. Plugins can publish contextual cast presence, sprite variants, entrances, exits, transitions, dialogue, and choices; Haruka Academy and Emberback include refreshed portrait-backed examples.
- Plugins can expose contextual slash commands and matching UI actions. Commands such as dice rolls and inventory access declare arguments and context needs in the plugin manifest, while typed command traces give text and button invocations one log and replay format.
- JSON Render surfaces support typed plugin components, directives, slots, repeats, tabs, and development inspection. The same plugin-owned action contract now works in panels, message blocks, stage choices, and command-driven navigation without framework branches for concrete plugin IDs.
- WorldIR provides one shared post-turn fact projection. Quests, inventory, affinity, codex, relationships, and world rules can consume the same evidence instead of independently re-reading the narrative.
- World generation produces richer portable packs. The builder can create plugin policy, character blueprints, lorebook content, dimensions, rules, memory blocks, and stage-aware settings through the same WorldData validation path used by authored worlds.
Changed
- Runtime context is fresher and more deterministic across long executions. Session state reads are deduplicated, context budgets and compaction preserve authoritative world and memory views, model-slot overrides propagate consistently, and snapshots retain the inputs needed for inspection and retry.
- WorldData import uses schema-aware projections. Canonical characters, lore, media, plugin namespaces, and WorldIR seeds are planned and synchronized through explicit targets with cross-source diagnostics.
- Stage choices and free-text actions share one contextual decision flow. Suggested actions remain convenient UI shortcuts without blocking the composer, and plugin UI can open or focus its matching surface through the same command pipeline.
Fixed
- Player-created worlds and their old sessions remain deletable after upgrade. Sessions saved before incarnation metadata existed now use a stable compatibility identity during guarded deletion instead of failing with a 500; world and session confirmation dialogs also accept
Enteras the confirm shortcut. - Default Playwright runs are isolated and deterministic. They use dedicated ports and a temporary MemoryStore, never reuse stale development servers, clean up their process trees, and require an explicit opt-in before calling live providers.
- Long-running memory and runtime work handles cancellation, retries, deadlines, and automatic snapshots consistently. Failed or resumed executions keep their relevant context without duplicating stale session reads or losing trace identity.
0.0.27 - 2026-08-26
This is a large release that rebuilds core settings, session, storage, desktop-networking, world-localization, and release infrastructure while hardening the complete player flow.
IMPORTANT
This release includes substantial database and configuration changes. A normal in-place upgrade is supported, but back up any custom worlds or plugins you want to keep first. If you encounter startup, configuration, or saved-session problems after upgrading, quit Covel, reinstall it, and reset its local state by removing the complete ~/.covel/ directory plus any separately configured data_root. The reset removes API keys, settings, saves, logs, and any custom content you did not back up. Official macOS and Windows artifacts are unsigned, so the operating system may warn on first launch.
Added
- Desktop requests can use direct, system, HTTP(S), or SOCKS5 proxy modes. The selected mode applies consistently to provider traffic and GitHub model-database updates, with the active route visible in settings.
- Provider failures now have actionable, expandable details. Authentication, permission, quota, timeout, network, and upstream failures receive localized guidance while preserving the full original error for inspection and copying.
- World discovery is locale-aware and custom worlds can be deleted in the client. Built-in worlds now resolve localized metadata, cast, rules, and presence consistently, while custom-world removal also clears its local media and cache state.
- Browser-local sessions use durable checkpoints. The client synchronizes complete session snapshots through the store contract instead of maintaining a second IndexedDB runtime implementation.
Changed
- The first provider model becomes both the story and plugin default. A fresh setup can run plugin-backed tasks immediately without requiring a second manual slot assignment.
- GitHub model-database refreshes report progress and results. Desktop refreshes go through the sidecar, update the bundled capability database safely, and surface start, success, and failure feedback.
- Settings and desktop configuration now share versioned, revision-aware persistence contracts. Atomic writes, conflict detection, strict hydration, and explicit config/data roots prevent partial or stale snapshots from silently replacing newer state.
- Session, runtime, and storage paths converge on current contracts. Turn control, suspension artifacts, plugin-data mutations, lifecycle cleanup, vector ingestion, and cross-backend transactions now use fewer duplicate state paths and consistent isolation rules.
- The bundled worlds and model workflow received a localization pass. World selection follows the active locale, provider model choices stay scoped to the selected provider, and Mistport, Haruka Academy, and Emberback content has been refreshed.
- The build and deployment stack is modernized. The workspace pins Node.js 26.7 and pnpm 11.22, updates Electron and the TypeScript/Vite toolchain, verifies unpacked desktop resources on both release platforms, and makes PostgreSQL integration coverage part of CI.
Fixed
- Windows desktop LLM requests no longer fail before connecting. Provider traffic now pairs npm Undici's fetch with its compatible dispatcher instead of crossing Electron's built-in Undici boundary, fixing the
fetch failed/UND_ERR_INVALID_ARGfailure across core providers reported in #44 and #45. - Session-scoped work is generation-safe across server Pods and reused IDs. Grants and pending decisions bind to a persisted incarnation plus per-plugin revocation generation; mutations revalidate owner, incarnation, and status under the backend lock; detached jobs validate every write; and deletion drains pending work and media access without leaving partial state.
- Undici requests share one validated outbound-network path. Proxy and DNS behaviour no longer depends on incompatible request-hook shapes, local/private targets keep their safety checks, and response bodies and dispatchers are released reliably.
- Settings and API-key snapshots persist in mutation order. A slow older write can no longer finish last and erase a newer provider, model, or secret configuration.
- Settings hydration is single-flight. Concurrent or repeated
SettingsStore.init()calls now share one load generation, so an older completion cannot overwrite a newer snapshot andready()cannot report a stale generation. - Browser-local restore and storage writes are coordinated across handles. Recreated session ids, vectors, root IndexedDB updates, and legacy snapshots can no longer overwrite or attach to the wrong generation.
- Runtime cancellation, capability scheduling, and plugin transitions are deterministic. Parent deadlines cancel recursive work, every declared capability need is honored, rejected executions preserve identity, and lifecycle events are emitted with transactional plugin-data changes.
- Chat and session surfaces remain usable during long runs. Scrolling work is bounded to the chat viewport, narrow error and runtime layouts wrap cleanly, and stale provider-test results are invalidated after credential changes.
- Provider and runtime errors remain readable at narrow widths. Model metadata and connection tests use separate rows, failed runtime chips wrap cleanly, and corrected API keys invalidate stale authentication-test results.
- Plugin README validation ignores package-manager residue. Ignored
node_modules-only directories no longer create false missing-README failures.
Removed
- The duplicate legacy state package and standalone IndexedDB session implementation are gone. Browser and server modes now rely on the shared store and session contracts, reducing divergent persistence behaviour.
0.0.26 - 2026-08-19
This release rebuilds provider and model configuration around explicit connection profiles, capability-aware reasoning controls, and safer provider-specific request handling.
Added
- Provider connections are first-class settings. Each connection has its own stable id, provider family, protocol, base URL, API-key namespace, and model list. Multiple proxies or accounts for one provider can coexist without sharing credentials, while existing presets migrate automatically.
- Reasoning effort follows model capabilities. Settings expose only the effort values supported by the selected model and translate them into the provider-specific request fields used by OpenAI, Anthropic, DeepSeek, and Qwen-compatible endpoints.
Changed
- LLM settings now separate connections, models, slots, and advanced request options. Provider catalogs normalize ids consistently, slot bindings clear stale reasoning overrides when their model changes, numeric fields can be cleared, and base URLs commit once on blur or Enter.
- Project documentation is organized by audience and lifetime. Stable user and developer guides live under
docs/; audits, implementation plans, temporary analysis, and handoff notes live underdevs/docs/. Desktop packaging and world-media generation now have indexed guides, and the architecture section carries the maintained technical-debt ledger.
Fixed
- Thinking models no longer emit incompatible tool-call payloads. DeepSeek V4 thinking requests omit
tool_choice, tool-call assistant messages retain non-null content, and streaming and non-streaming paths share the same compatibility rules. - Reasoning controls reject unsupported combinations before they reach providers. Claude 4.0 no longer receives newer Anthropic effort fields,
gpt-5-proexposes onlyhigh, and Qwen thinking-only models cannot be forced into disabled thinking. - Provider migration preserves independent credentials and model references. Canonical provider ids no longer collapse distinct connections, request routing uses the connection namespace, legacy family-level keys remain available as a fallback, and orphaned preset secrets are cleaned up.
0.0.25 - 2026-08-11
This release hardens the complete default game flow, from choosing a world and running setup plugins through narrative execution, post-turn plugins, persistence, and client recovery.
Added
- Transaction-scoped execution journals. Runtime messages and domain proposals are buffered until the turn finalizer commits, so a failed commit leaves no partial player message, interaction, or plugin write behind.
- Framework audit remediation records and logging self-checks. The P1/P2 data-flow plans, architecture diagrams, server log-tee coverage, and desktop logging self-check make the new contracts explicit and diagnosable.
Changed
- Setup runtimes pass world schemas through explicit same-execution outputs. Character creation no longer depends on reading uncommitted world-init state from the store; downstream setup runtimes consume the scheduler-visible result instead.
- Post-turn memory uses authoritative committed context. The executor refreshes session context after commit, resolves localized character labels, and applies deterministic merge rules so the current turn's form and character values reach memory intact.
- Quest updates preserve stable identities.
core-questassigns durable ids and semantically merges repeated extraction results, preventing duplicate quests and accidental state resets across turns.
Fixed
- Local session bootstrap now mirrors the complete server contract. Active plugins, locale, model overrides, and lore overrides are synchronized before the session becomes active, eliminating first-turn configuration races.
- Turn commit and SSE failure handling are atomic end to end. Interaction-record failures roll back the entire finalize transaction; failed terminal events clear optimistic streamed text and leave the client in a settled state.
- Action, form, and plugin-RPC boundaries reject malformed or conflicting input. Discriminated action parsing validates identifiers, locales, models, submitted field types and options; form submissions are transactional and idempotent; plugin RPC actions serialize on the session lock.
- World-init writes follow the runtime proposal contract. Guards inspect the execution write buffer, batch writes respect schema availability, and setup no longer relies on direct store mutation.
- Server and desktop logs retain complete lines reliably. Log tee writes, shutdown behavior, and desktop log configuration now preserve diagnostics across the supported launch paths.
0.0.24 - 2026-08-01
Added
- Four RPG gameplay plugins:
dice-check,core-quest,affinity,inventory. The bundled set covered knowledge (codex), relationships (npc-graph), and guidance (guide), but nothing made an RPG play like one: success was narrative fiat, goals lived nowhere, items evaporated, and feelings were prose-only.dice-checkis a zero-LLM pair — a pre-turn roller injects a pre-rolled d20 pool + check rules into the narrative engines, and an event recorder persists the receipts the narration sends back (🎲 message block + right-panel log). The receipt event is batched by contract (check.resolvedcarries achecks[]array):emit-eventdedupes by topic per turn, so per-check emissions would silently drop everything after the first. The other three are post-turn extraction agents on the codex template (dual-engine narrative inject + own-state inject,needs: narrative-engine), each with a name-first batch tool, world-importabledataSchemas, a right panel, and a per-turn message block. Inventory additionally ships a player-sideitem-opRPC — equip / unequip / drop buttons on the panel; using an item deliberately stays in the story loop, where its consequences belong. - Emberback (鳌背孤城·烬海纪), a third flagship world built for the RPG suite. The last great turtle carries the last city across an ember sea — and halts, without warning, above an uncharted sunken spire. Five 0-5 check attributes feed dice modifiers; worldData seeds 3 quests, 6 items (scale-coin currency included), and 3 NPC affinity records, all schema-validated against the plugins and name-aligned with the lore; plus a portrait-backed 3-NPC main cast, 6 living-world rules, and 2 world memory blocks (
descent_log,turtle_state). - LLM concurrency gate. A post-turn stage that fans out seven agents used to fire seven simultaneous LLM calls; slower providers congested until every call timed out. A process-wide FIFO semaphore (
COVEL_LLM_MAX_CONCURRENT, default 4,0disables) caps in-flight calls, and queue waits extend deadlines end-to-end — through the retry loop and the agent tool loop — so waiting for a slot is the framework's cost, not the runtime's budget. Observed live: a tracker that burned its whole 2-minute budget queueing now completes in seconds. - Scoped runtime retry replays the original turn's context. A bare manual trigger resolves
input.inject/needsempty, so retrying a failed post-turn agent used to burn tokens extracting from nothing.retry_runtime+retryFromTurnId(and the plugin-rpc equivalent) now seed the execution with the source turn's persisted runtime outputs. Seeds are context, not products: dropped before the event fan-out (they would replay the source turn'sscene.set/ receipts / generation requests) and never re-persisted. The execution strip's failed-chip ↻ button uses this path and keeps the turn's narrative intact.
Fixed
- Returned agent failures now always emit
runtime.failed. Four failure paths (deadline without output,requireToolUseunmet, tool-failed, schema short-circuits) returned a failed result without any terminal event — the DB recorded the failure while the execution strip spun forever. Emission now lives in the single funnel every returned failure routes through. - Chat auto-scroll can no longer shove the page off-screen.
scrollIntoViewon the bottom sentinel scrolls every scrollable ancestor, including the document; any transient page overflow while a session loaded left the whole app shell scrolled out of the viewport with no way back. All follow paths now scroll the tracked chat viewport directly.
Removed
player-identityis archived (plugins/_archive/, not loaded). Voice/persona moved to the character card long ago — set at creation via worldcharacterAttributes, injected as{{ player.character }}— leaving the plugin UI-less, preset-less, and resolving an always-empty persona. Thepersona-providercapability hook and shared types stay for third-party providers.
0.0.23 - 2026-07-28
Added
userSettingscan declaretype: slot. Whether the frontend rendered a model-slot picker used to depend on the setting being literally namedmodelPresetId— an undocumented convention discoverable only by reading framework source, and one that produced a picker on the prep screen but a free-text box under Settings → Plugins for the same setting. Discovery now keys off the declared type, so the setting can be called anything. The value stays a bare string rather than an enum, so a slot added after boot is still accepted.- Colliding
userSettingsacross a plugin's runtimes are caught at load. The storage key is plugin-scoped (plugin.<id>.<key>) while the declaration sits on a runtime manifest, so two runtimes of one plugin could declare the same key and silently disagree. Identical declarations now dedupe quietly; diverging ones warn and keep the first.pnpm validate:pluginon a plugin directory runs the same cross-runtime check.
Removed
relationsentries are plain plugin-id strings; the object form is gone. It had accumulated four interchangeable ways to spell one plugin id ({plugin},{runtime},{target: id},{target: {plugin}}) alongside the bare string every bundled plugin actually used, plus three fields nothing read:typerestated the array the entry already sat in, andoptional/reasonhad no consumer at all (optional: trueunderrequireswasrecommendsspelled differently). Dependency rationale belongs in a YAML comment above the entry — which is what the bundled plugins were already doing. A third-party manifest using the object form now fails to load with a named migration hint rather than resolving to something unintended.relationsno longer acceptscapabilityortagtargets. Both parsed cleanly — they were in the schema, the types, and the reference docs — and were then ignored forever, because relations resolve throughrelationPluginId(), which only ever handled plugin ids. The repo's own schema fixture demonstrated the trap: it declaredrequires: [{ capability: "scene-cast" }]wherescene-castis a plugin id, not a capability, so the author meant a dependency and wrote a form that could never deliver one. A documented option that never works is worse than no option. Manifests still using them now fail to load with a migration hint instead of losing the dependency at runtime. Capability-based scheduling dependencies are unaffected —needs/afterare a separate mechanism.
Changed
- Working memory renders after the plugin instructions instead of before them. It is the one system-prompt segment that changes every turn, and sitting between the framework preamble and the instructions it invalidated everything downstream: the instructions are usually the largest block in the prompt and were re-billed every single turn purely because a few lines of memory ahead of them had changed. Both caching models paid this — an explicit
cache_controlsegment is only reusable when its whole body is byte-identical, and an automatic prefix cache stops at the first differing byte. The instructions now own a cacheable segment by themselves, and working memory trails the final breakpoint as an uncached tail. Segment numbering is unchanged (it is the segment's identity); only render order moved. Placing it last also puts the turn's freshest state nearest the conversation, where a model weighs it most heavily. This changes what every runtime sees and in what order, so it warrants a real-model check before release — the mechanical behaviour is covered by tests, prompt quality is not.
Fixed
A background job orphaned by a restart is reported instead of spinning forever. Background runtimes (scene backdrops, generated illustrations, TTS) run in-process, so killing the process kills the callback while its
_jobsrow stayspending— the panel showed "generating…" with nothing behind it, permanently and silently. The boot sweep meant to clean this up decided by age, and its predicate read the row's enqueue time, so it could not tell a corpse from a job legitimately still running; raising the threshold only traded false kills for longer spinners, which is why an earlier attempt to run the sweep periodically was reverted. Liveness is now answered by ownership rather than elapsed time: each pending row is stamped with the id of the process that owns it, and a freshly booted process cannot own any pre-existing row. A running job is therefore never reaped by construction, no heartbeat write is needed, and an orphan is failed the moment the server comes back rather than 15 minutes later. Orphans are failed rather than re-run — re-running bills the player again for image or speech generation, and the request-scoped settings a faithful re-run would need are not stored on the row — but the trigger context is carried onto the terminal row so a retry can be offered deliberately. Note this makes the sweep exact for one process per store, which is every deployment Covel ships today; running several instances against one database would additionally need a lease, since a booting instance now reads another instance's in-flight rows as foreign rather than granting them the old grace window.Generating a scene backdrop no longer blocks the next message. A deferred background follower ran its entire turn inside the session lock, and for the media plugins that turn is a provider call —
background-genallows six minutes and one image genuinely takes 60-300s. Anything the player did meanwhile queued behind the artwork. Execution now runs outside the lock and only the commit takes it, so the wait drops from a whole generation to a transaction. The three reasons the lock was originally taken do not apply to this path: it passes no session clock (andcompletedPlayerTurnscounts only player-origin executions), its domain writes are buffered intofinalizeExecution's single transaction rather than dribbling out mid-run, and a follower appends no turn messages. What did need the lock — the commit and the post-commit snapshot, which must observe a settled session — still holds it. Two guarantees are preserved explicitly: followers of the same runtime are serialised on a<sessionId>::<runtimeId>job lock, so the "already generated?" checks that prevent double-billing stay atomic; and the session status is re-read under the lock before committing, so results are discarded rather than written into a session the player paused or ended during the minutes the job was running.The Docker image builds again, and its
llm.tomlis actually read. The image had not built since 2026-07-21 and its slot config was a silent no-op, from three independent breaks: the per-member manifestCOPYlist went stale when three plugins landed, sopnpm install --frozen-lockfilefailed on a missing workspace importer (nowCOPY --parentsglobs, so a new workspace member is covered automatically instead of drifting again); Node 25+ no longer bundles corepack, socorepack enable pnpmexited 127 right after the Node 22 → 26 bump (corepack is installed explicitly, still resolving pnpm from the rootpackageManagerfield); and the server resolves./llm.tomlfrom its cwd/app/apps/serverwhile both theCOPYand the compose mount target/app/llm.toml, so every slot definition was ignored and the server fell back to built-in defaults (COVEL_LLM_TOMLnow points at the path the mount actually uses).Losing a race for the session lock returns a retryable 503 instead of a server error. Under PostgreSQL the lock acquire budget is 30 s while a background image generation legitimately holds the lock for minutes, so acting during generation hit the timeout on an entirely normal path. Nothing caught
SessionLockTimeoutError, so it reached the global handler and surfaced as500 Internal server error— indistinguishable from a real fault in logs and monitoring, and giving the client no reason to retry. It is now a503 { code: "session_busy" }, which is whatdocs/v2/reference/api.mdand this changelog already described for manual snapshots and forks; the behaviour simply had never existed. The raw message names the session and internal module, so only a fixed string crosses the wire and the detail stays in the log.Saving a theme with a Chinese name no longer deletes the previous one.
slugifyThemeIdstrips every non-[a-z0-9]character, so an all-CJK name reduced to an empty string and fell back to the shared constantcustom-theme— 我的极光, 我的纸本 and 夜读模式 all produced the same id.ensureThemeIdonly de-duplicated against builtin ids, andsaveCustomThemewrites withfilter(id !== …).concat(…), so the second save silently dropped the first theme with no warning and no undo. In a Chinese-first product that is the main path, not an edge case. The fallback is now derived from the name itself, which keeps re-saving the same name an update (same id, as intended) while two different names no longer collide.Colour swatches announce which token they belong to. All 48 shared one hardcoded
aria-label="colour", so a screen-reader user heard the same word 48 times with no way to tell the page background from the danger accent. Both the swatch and its paired text field now carry the token's own label. The theme-name field gained an accessible name too — it had only a placeholder, which is not a reliable one and vanishes on input.Reading theme defaults ignores an in-flight drag preview.
readTokenDefaultslifted only the properties the override module had written, but the controls also paint directly to the element while a slider is being dragged. Inside the 200 ms commit debounce those two views disagreed, so a theme or scheme switch landing in that window could record a half-dragged preview as the theme's own default. It now lifts every adjustable token regardless of who wrote it.
0.0.22 - 2026-07-26
Appearance stops being a three-way switch. The token system was already complete — every theme defines it and every atom consumes it — so what was missing was never the plumbing, only a way for the player to reach it. This release exposes 45 of those tokens grouped by the region they affect, adds a path from "I tuned it" to "here is a theme package", and publishes turn state to the DOM so a theme can finally react to what the game is doing. The rest is player-facing correctness: two inputs that accepted what they shouldn't, and one that rendered less than it should.
Added
- Settings → Appearance: 45 adjustable design tokens. The panel previously offered a theme picker, a light/dark toggle, and a CSS file import — anything finer meant hand-writing a theme. Tokens are now grouped the way a player thinks about the screen (narrative type, interface type, region backgrounds, text and rules, accents, ambience, shape, layout sizes) rather than by variable name, and the whole surface is driven by one declaration table (
token-schema.ts) — adding a knob is a row, not a pipeline. Overrides apply as inline styles on<html>, which outrank any stylesheet, so clearing one restores the theme's value exactly rather than recomputing it. Colours are stored per scheme and sizes/fonts shared: the same colour is rarely right in both light and dark, but "I want bigger text" is not a per-scheme preference. Dragging a slider paints straight to the DOM and only the settled value reaches storage — persisting every frame would write localStorage 60×/second. The font list includes CJK serif/kai/hei stacks, which is what makes long-form Chinese prose readable and is not available from a generic system stack. - Save the current look as a theme package. Closes the gap between the panel and hand-written CSS: bake the active theme plus every override into a standalone package, then export it, share it, or open it and add effects by hand. The snapshot covers the whole token contract rather than only the overridden tokens — a package carrying just the changes would fall back to base values everywhere else, so "save what I see" would visibly change what you see. Theme ids stay off the builtin namespace (a custom theme claiming a builtin id is dropped on the next registry sync, so saving one would appear to work and then vanish), a single-scheme source stays single-scheme, and generated declarations drop any value containing
;or braces, since that CSS is re-parsed as a theme and such a value can close the rule early and escape its scope. - Turn state is published to the DOM for themes to hook.
<html>now carriesdata-turn(idle/executing/waiting/error) anddata-session, so a theme can style what the game is doing — an input that breathes while the model writes, an alarm pulse on failure — which pure CSS previously had no way to ask about.waitingoutranksexecuting: the turn is still open but the kernel is blocked on the player, and that is the state worth highlighting. Deliberately framework-owned and coarse; plugin-specific state (scene, mood, combat) needs a declared capability channel rather than hardcoded plugin ids, and is not part of this. aurora, a reference theme for effects. Glassmorphism, an@property-driven drifting gradient, message entrance animation, hover glow, and the turn-state effects above — as a builtin the player can just pick, and as the thing to copy when writing one. It passes the same import validation a player's theme goes through, so copying it cannot land you outside the scope rules; a test holds that.- The studio rail starts collapsed and remembers its layout. The left rail holds studio configuration — plugin toggles, model slots — none of which a player acts on while reading, yet it opened at 20% every session and left the narrative column about 40% of a 1440px viewport. Collapsed it gets 69%, and whatever the player does next survives a reload, stored through
useDefaultLayoutrather than hand-rolled localStorage. Mobile and desktop keep separate entries, since the mobile group stacks vertically and puts the right rail in a different slot.
Fixed
- Player messages render as markdown. They went through a bare
<p>, so markdown syntax showed as literal characters and real newlines collapsed into spaces — choice text carrying its own line structure arrived as one flat paragraph. They now use the same renderer as the narrative, with lone newlines promoted to markdown hard breaks first (markdown otherwise folds a single newline into a space, so the player's Enter key silently did nothing). A literal backslash-n is deliberately left alone: that is over-escaped model output rather than a break, and rewriting it in the view layer would hide a data bug and misrender any text discussing\n. - The composer is locked until the adventure begins. The "begin adventure" hero renders on
messages.length === 0 && !executing && isPreGame, but the composer only disabled itself for an unanswered interaction block — two unrelated conditions, so the button sat waiting to be clicked while the input stayed live. Sending from there opened a turn before any setup runtime had run, leaving the narrator to answer in a world with no character and no opening scene. Folding the condition intocomposerDisabledcovers the Enter key too, sincehandleSubmitalready guards on it. Kept out ofcomposerBlocked, which means "an interaction is unanswered", drives its own placeholder, and is what e2e reads viadata-blocked. The pre-game predicate now lives in one place shared by the hero and the composer — two copies of the same rule drifting apart is exactly how the gap appeared. - Author-facing detail stays out of the player view. Every message header carried its full 36-character turn UUID, meaningless to a player and a whole line on a phone, while every other author-facing detail in the same file was already gated on
viewMode. It now follows the same rule. The stage dialog box is clickable to advance or skip the typewriter but had onlycursor: pointerbehind it — no hover state, and no accessible name, so a screen reader announced an empty button.
Changed
- CI stops invalidating its own test cache.
testlistedplugins/**as an input, which pulled each plugin's run artefacts — vitest'sresults.json, turbo'sturbo-test.log— into the hash. Running the suite rewrote those files, so the next run hashed differently and all 43 packages missed; running tests was what invalidated the test cache, and it never settled above 2/43. Explicit input globs do not consult gitignore. The build step also moves behindpush(scoped to main): it covers the bundling layer thattsc --noEmitcannot see, which is worth keeping but not ~2 minutes on every PR push.
0.0.21 - 2026-07-25
A reliability pass over the web client, from exercising the front-end the way a player does rather than the way its tests do. Nothing here adds capability. Three of these fixes were destroying persisted data, four left the player unable to act, and two let a single bad plugin or theme take the whole screen down.
Fixed
- Stored settings survive a failed read or write. Two independent, silent data-loss paths.
set()mutated the in-memory map and then awaited the adapter with no rollback — every UI call site isvoid setValue(...), so a quota exception (importing a multi-MB CSS theme, say) left the map ahead of storage with no signal, and every later write re-serialised the same poisoned map and failed the same way. Separately, a failedload()was indistinguishable from "nothing stored yet"; sincesave()always writes a full snapshot, the next single-setting change overwrotesettings.jsonandkeys.envwith just that one key. Rollback undoes only the key it touched (a whole-map snapshot would revert a concurrent write that had already succeeded) and stays synchronous so read-after-write still works in the same tick. A failedinit()now recordshydrationErrorand refuses writes includingclearAll— a hydration failure shows defaults, which reads as "my settings are gone", and the player's natural response is Reset. The REST backend's GETs were missing the bearer header its PUTs already send, so a tokened desktop install 401'd on every boot and hit exactly this path. - A reload no longer clobbers state-patch history.
addStatePatchread the in-memory map — empty after a page reload — and wrote[...list, patch]back to IDB, overwriting the persisted array. Playing, reloading, then taking one more turn destroyed the session's entire state-patch history. It now reads through IDB before appending and re-reads the map after the await: one turn commonly commits severalstate.changedevents in a single flush and the SSE handler appends fire-and-forget per event, so without the re-read two appends in the same tick lose one.syncToServeralso no longer swallows message-sync failures — the next turn would run against empty server history and the narrator would visibly forget the story. - SSE events and stream settlement are scoped to their own session. Nothing aborted the previous action stream when the player switched sessions mid-turn, so envelopes for session A kept arriving while B was loaded. Only the narrative-delta path was guarded; every other event type was applied to B, and
narrative.completedpersisted A's text under B's session id — permanent in local/IDB mode, still there after a reload. The event handler now drops mismatched envelopes (failing open when either id is absent, so the resume path is unaffected), andfinalizeActionExecutiontakes the session its stream was opened for — without it the old stream's completion cleared the new session's executing flag. - Suggestion panels no longer lock the player composer. Suggestion actions (
draftMessage/selectSuggestion) were classified as pending interactions alongside must-answer ones (submitForm/selectChoice), so any turn wherescene-promptsorguideoffered shortcuts disabled the main composer for the rest of the turn. The player could only pick a suggestion or use the plugin panel's own secondary input, with the most prominent input on screen dead and captioned "finish the current interaction first". Queued drafts no longer block either: the composer stays usable while selections sit in the draft bar, and submitting sends the selections and the typed line together as one turn — the behaviourui-panels.mdalready documented for the input bar. - Mobile navigation is wired up. The hamburger button had an
aria-labeland an icon but noonClick, while the desktop nav and the language toggle are bothmd:only. On a phone the top bar was decorative: worlds, sessions, plugins and debug were unreachable and the language could not be switched at all. It opens a Radix Dialog now, which brings the focus trap, Escape handling andaria-modala hand-rolled dropdown would have to reimplement. A duplicateopen-pluginssubscription insession.tsxalso held its own dialog instance that was set open but never rendered, then popped up unbidden on the next navigation back to world select. - AI request headers encode as UTF-8 base64.
btoa(JSON.stringify(...))throws aDOMExceptionon any codepoint above U+00FF. Both payloads carry free user text —X-Slot-Configembeds the user-typed custom preset name,X-Plugin-User-Settingsarbitrary per-plugin values — and the app's default locale iszh-CN, so naming a preset in Chinese made every request fail. - API failures are classified instead of collapsing to null.
request()threw a bareError, soRemoteDataServicecould not tell 404 from 401 or 500 and mapped everything to null: a hosted player whose owner token had expired was told the session did not exist.ApiErrorcarries the status, and only a genuine 404 becomes null, matchingLocalDataService. - A broken plugin surface no longer takes down the game view. The only error boundary was
AppErrorBoundaryat the app root, and the server validates just the spec envelope (view: z.record(z.unknown())), so anything insideviewreached the catalog renderers unvalidated. Plugin UI specs are not gated by the server-code approval flow, so a third-party typo — or an LLM-emittedui.renderblock — replaced the whole game view including a still-streaming narrative, and because the offending data is persisted, a reload crashed again. AddsPluginSurfaceBoundaryaround the panel renderer andui.renderblocks, a null-prototype component registry (so a spec namingconstructorhits the unknown-component fallback instead of resolving toObject), guards at the shared resolve chokepoints, and depth caps in the JSON renderers. - Theme CSS scoping is enforced, and only the active theme is mounted. Validation only required that at least one scoped selector existed and silently ignored the rest, so
html[data-theme="x"]{} * {display:none}passed. Worse,syncThemeStylesmounted every registered theme at once, so an imported theme the player never selected still applied any rule that escaped its scope — and kept applying it after a restart, hiding the settings dialog needed to remove it. Only builtins plus the selected custom theme are mounted now; that, not the scanner, is the actual containment.@importis banned outright (a remote fetch from a theme pack doubles as a "this player opened the app" beacon), and a custom theme may no longer claim a builtin id. - i18n gaps closed and browser language detection actually applies. Seven keys were used in code but defined in neither locale, so
zh-CNplayers read the inline English defaults; three of them could never have worked, being used as both a string and a namespace.resolveInitialLocale()ran butui.localedefaulted to a hardcodedzh-CNandmain.tsxapplied the store value unconditionally, so an English browser flashed English and then flipped to Chinese permanently. The coverage gate now also flags CJK punctuation reaching en-US verbatim and checks that every statically-analysablet()key is defined in both locales — its per-line regex had been missing 73 of the 745 keys because prettier wraps long calls.
Changed
- First paint ships 580 kB less JavaScript. Measured on a real build: eager JS drops from 1.99 MB to 1.41 MB raw (566 kB to 405 kB gzip), entry chunk from 1.21 MB to 906 kB.
react-force-graph+ d3 had a manual chunk, and rolldown put a shared React CJS interop module in with them — every chunk then statically imported 194 kB of graph code, defeating thelazy()entirely; deleting the rule lets rolldown isolate them.yamlwas welded to zod the same way. Routes were not code-split at all. ThreemanualChunksrules matched packages that are not installed. The landing page also loaded a 10 MBdemo.gifwhile the 1.3 MB MP4 thatbuild-mediagenerates for exactly this purpose sat unused — the GIF shipped in every deploy and in the Electron installer, and is deleted here. - Session E2E waits for the turn it claims to wait for. The specs inferred turn state from
input:disabled, which is wrong in both directions: the composer stays enabled mid-turn (submitting steers the running turn), so the wait passed within its fast path without ever waiting; and the composer was located viainput[type=text]").last(), which only held as long as no plugin panel rendered an input after it. The composer now exposesdata-executinganddata-blockedseparately, the helpers both specs had copy-pasted live intests/e2e/helpers/player.ts, andexpectPlayerCanActasserts the invariant this release's composer fix restores.
0.0.20 - 2026-07-25
A follow-up to the cleanup release, from watching real sessions run. Three fixes, all found by reading traces rather than tests: a runtime could report success having done nothing, a hazard warning fired every turn regardless of the truth, and several runtimes were handed tools their own prompts told them never to call.
Fixed
- A bare
runtime-doneno longer satisfiesrequireToolUse. A runtime whose whole declared job is to call a tool could finish as a success having called only the framework's loop terminator. Seen in production withscene-prompts: the model answered with prose, the gate nudged it once, and the model replied by callingruntime-donewith the reason "完成 scene prompts 生成" — the actual work tool was never called. The turn reported success in 47.6s and the player got an empty quick-reply panel behind a green check. Two paths let it through: the gate counted any successful call, terminator included, and the runtime-done early exit breaks the loop before the gate runs. Both now require a business tool call, and an unmet contract finalizes asfailedwith a diagnostic instead of an empty success. - UI effects hazards are reported per slot, not per plugin. Any manifest declaring a
uiblock was credited with writingui:*, so every pair of UI-declaring runtimes in a DAG layer was flagged — nine warnings in a three-turn session, all false: a right-panel plugin cannot overwrite a message block.EffectResourcenow admitsui:<slot>and the derivation emits one key per declared slot.ui:*still wildcard-matches them, so an explicit declaration keeps its meaning, and two plugins that really do share a surface still hazard.
Changed
- Runtimes no longer receive tools their prompts forbid. Five declarations were paid for twice — once in the tool schema on every LLM call, once in the prompt text spent forbidding them.
character-trackerdroppedlist-characters(its roster is injected as<existing-characters>), both imageprompt-generatorruntimes droppedplugin-data-list(they inject thepromptsnamespace), andworld-init/schema-gendroppedplugin-data-get/plugin-data-list(it writes during setup and never reads back).get-characterandlist-npc-graphstay: unlike the others they are the documented escape hatch for a truncated snapshot, and a live session confirmedget-characterbeing used exactly that way.extractor's guidance, which said "无需调用" in bold and then described when to call it eight lines later, is now one instruction.
0.0.19 - 2026-07-25
The cleanup release, following a full audit of the core framework. Nothing here adds capability; it removes surface that was declared but never honoured, and closes the security gaps that hid behind it.
Breaking — six PLUGIN.md fields no longer parse. A manifest declaring any of them fails to load, so check yours before upgrading. None of them did anything at runtime, so removing them changes no behaviour except that the declaration is now an error instead of a silent no-op.
Removed field Replacement prioritystage(setup/pre-turn/narrative/post-turn/audit)upstreamRequiredneeds(same entries; turn-scoped by default)jobStatus— (its legacyViewsprojection had no reader)tools.localregister in the entrymodule viacovel.registerTool, then list the names undertools.pluginsuspensionSafe— (no handler replay exists to guard) rpc streaming— (dispatch is always synchronous; progress travels as plugin-data.changedSSE from a background job)
priority,upstreamRequiredandtools.localproduce a load error naming their replacement. One sharp edge inherited from the rpc parse policy: an unrecognized key skips the wholerpcblock rather than the offending action, so a plugin that declaredstreamingloses every rpc action at once — the warning names the file, line and key.
Added
- The test-runtime harness loads entry-registered tools.
pnpm test:runtimeruns a plugin'stests/runtime-cases.jsonthrough the real turn executor, so a case can have the mock LLM call the plugin's own tools — it never could, because the harness only ever readtools.localand every plugin moved toentryin v0.0.14. The concrete cost was the scaffolding template:templates/plugin-with-toolsships a case whose LLM callsrecord-note, so a new author's firstpnpm test:runtimefailed on a tool that was never registered. The harness now runs theentrymodule with aPluginAPIthat implementsregisterTooland no-ops hooks / RPC / wires (server-bootstrap concerns a single-runtime harness turn never reaches), and a name collision with a framework tool throws rather than quietly shadowing it.
Fixed
- Tool calling on the Anthropic adapter.
createAnthropicMessagesAdapternever serializedparams.toolsand dropped every tool-role message, so an agent runtime routed to ananthropic-messages-v1slot sent no tool definitions, got no tool calls back, and produced no proposals — no character updates, no plugin data, no image generation. It degraded to plain narration with nothing logged as an error while the protocol registry advertisedfunction_callingall along. create-notificationrenders again. The tool returned{ notified: true, level }, which the runtime never promotes (only a result carryinguiorinteractionis proposed) — the model was told the notification succeeded and the player saw nothing. It now returns the nestedAlertspec the renderer expects.- SVG media is rejected on upload and sandboxed on read.
POST /api/mediaacceptedimage/svg+xmlunder itsimage/*check andGET /api/media/:idechoed the MIME back with noContent-Disposition; a session owner could upload a scripted SVG, get a correctly signed URL, and read the web tier's localStorage provider keys from the app origin. Stored rows predating the ban are served as sandboxed attachments. - Provider-key availability requires the operator token.
GET /api/provider-keysgated only its raw-key branch, so on demo/commercial an anonymous caller still got a masked listing revealing which providers were configured plus the first and last four characters of each key. The whole route is now behindcheckHostedOperator(a strict no-op on self/desktop). - Community local-tool factories get the deny-all store. The unified
entrypath already scoped a community plugin's store, but thetools.localfactory path injected the raw DataStore for every trust tier — a community plugin could calllistSessions()or touch any session's plugin_data, bypassing proposal → validate → commit. Both paths now sharescopeStoreToPlugin. - Media-GC scans page soundly.
buildProtectedMediaIdspaged four lists by offset over a non-total order (two had noORDER BYat all), so a concurrently-upserted row could slip between pages and lose its media id — a destructive cleanup would then delete still-referenced bytes. All four now order by(createdAt, id). - Locale variants inherit what they omit.
reconcileValuetreated any field aPLUGIN.<locale>.mddid not declare as drift, forcing every translation to mirror the whole canonical manifest — and a mirrored manifest goes stale silently. An omitted field now inherits; a field declared with a different value still overrides and still warns. - Unknown-field load errors name the field. Zod reports
unrecognized_keyswith an emptypath, so every unknown-field failure fell through to the "ensure the file begins with---" hint even though the frontmatter parsed fine. The hint now readsissue.keys, and removed scheduling fields point at their replacements.
Removed
- Legacy scheduling fields.
priority,upstreamRequiredandjobStatusare rejected by both manifest schemas — a PLUGIN.md declaring any of them fails to load with a hint pointing atstage/needs. The compat fold (stageForPriority,aliasUpstreamRequired) and the never-readjobStatus.legacyViewsprojection are gone, and theRuntimeManifesttype no longer carries the fields. - Inert manifest fields
suspensionSafeand rpcstreaming. Both were accepted and stored but never read: no handler replay exists (resumeSuspendedRuntimere-enters the shared agent tool loop without re-invoking the handler), and rpc dispatch is unconditionally synchronous. Removing them changes no runtime behaviour. - The
tools.localregistration path. It was the last loader that imported plugin code by frontmatter path. Plugin tools register in theentrymodule (covel.registerTool) and a runtime declares their names undertools.plugin; every bundled plugin moved in v0.0.14 and the field's own doc had promised removal in v0.0.17. Deleting it also removes — rather than patches — the trust asymmetry the audit found: theentrypath scoped a community plugin's store to a deny-all view while this one injected the raw DataStore.local-tools.ts(240 lines) collapses toplugin-tool-access.ts(23);activatePluginServerCodenow does exactly what its name says. - Four designs that were drafted and never wired —
prompt-delta(+RuntimeOutput.metaData.rawPromptDelta),runtime-slot-resolver, and the json-render presets. Each was reachable only from a barrel export and a test that supplied the input no producer ever supplied.GET /runtime-outputs/:id/full-promptnow returns history and states plainly that the system prompt and injected segments are absent;trace_eventsremains the exact path.
Changed
- Two identifiers renamed to match what they do —
NormalizedRuntimeSpec.provenance.legacyFields→derivedFrom(it records only live normalizations now), and theactivatePluginLocalToolsHono context key →activatePluginServerCode. - Redundancy the audit flagged. The snapshot payload builder read every
turn_resultsrow for a session to build a plugin-id set, then filteredplugin_databy it — a filter that could never drop anything, since the set is the union of both sources. Long sessions paid a full-table read (each row carrying its complete runtimeResults payload) for a no-op. Also folded away an identity function (summarizeStorageMigrations→ theSTORAGE_MIGRATIONSconstant), a nine-linevector-factory.tswhose comment explained there is no factory, a second identicalTimeCursordefinition, and the unconsumedResolvedTool/ToolClient/ToolDefinitiontypes. - Ponytail debt is now a ledger. This release introduced the first 19 entries in the maintained technical debt ledger, each with an accepted ceiling and an observable upgrade trigger. Six rationale-only markers were downgraded to plain comments so the ledger stays focused on work actually owed.
- Docs realigned to the shipped surface. Removed the deleted
POST /api/sessions/:id/turnendpoint from the API reference, corrected pointers at moved/deleted modules, and rewrote every passage that described removed features as still-live compat — the reference docs now describe the current shape only, with history left to this changelog.
0.0.18 - 2026-07-24
The scheduling release. The numeric priority scheduler is gone: runtimes now declare a named stage (setup / pre-turn / narrative / post-turn / audit, strict barriers between stages) plus typed dependency edges — needs (gate + DAG edge, by runtime id or capability), after (ordering only), and typed inputs bindings resolved into provenance-wrapped ctx.inputs. Session lifecycle truth moved to a dedicated clock (phase / completedPlayerTurns / setupRuntimes) with turnCount / preGameCompleted derived at read time, and turn counting became single-writer (turn-wide finalize transaction + an idempotent logical-turn ledger). A same-day extensibility audit closed every "declared but silently ignored" manifest surface it found.
Added
- Stage-driven scheduler — per-stage DAGs with strict barriers; independent runtimes in a stage run in parallel (name breaks ties); dependency cycles are diagnosed and disabled for the turn instead of silently run in arbitrary order. Band selection is by
session.phase, not turn number. - Session clock + setup state machine — per-runtime setup mirrors with retry budgets, generations (a plugin version bump invalidates a stale
done), and ablockedterminal;needs(scope: session)cycles are blocked up front with a full path diagnostic, and the positive gate holds a consumer back until its producer isdonein the frozen snapshot. - Opening continuation — the request that completes the last setup runtime chains exactly one main-loop turn on the same SSE stream, so the narrator's opening arrives without a second player message.
- envelope-v1 handler results — discriminated
success / suspended / skipped / failedoutcomes with unified normalization; non-success envelopes carry observation channels only. All bundled function handlers migrated. recordAsexports andruntime-exportinjects — versioned cross-execution publications with frozen-at-execution-start reads.- Kernel job-status channel —
ctx.progress.report()streams append-only, idempotent progress events (SSE + store) for long-running work; reported jobs are terminalised from the execution outcome. - Effects declarations — derived read/write sets with a same-layer hazard policy (
effects.reads/writes/parallelSafe), plus declared HTTP permission upper bounds (permissions.http). pnpm validate:plugin— one command runs the loader compat parse (line-numbered errors, I18nText folding) and the strict authoring schema (missingstage/ legacy fields rejected);--compatcovers legacy third-party manifests.schedulable-missing-stagediagnostic — anauto/scheduledruntime with neitherstagenor legacyprioritynow warns at load instead of silently never running; pure registration surfaces (ui / hooks / entry / wires) are exempt.- Media plugins and wires — bundled
dashscope-image-gen/openai-image-gen/mimo-ttsplugins; wan2.6/wan2.7 multi-image support and qwen-image-3 on the DashScope wire; shared image-generation trunk extracted intoplugin-handlers-utils.
Changed
- Legacy scheduling fields are compat-only —
priorityfolds to a derivedstageandupstreamRequiredaliases intoneedsat a single normalize point; all bundled plugins single-declarestage+needs(two documented loader-gatedpriorityexceptions remain). Event fan-out orders subscribers byname, not priority. turnCount/preGameCompletedare frozen legacy columns — API responses derive them from the session clock at read time; response shapes are unchanged.- Manifest
descriptionaccepts I18nText — the preferred locale-map form now validates on both schemas (the loader folds to a single string post-parse), so editor tooling no longer flags every bundled manifest. - Docs and skills realigned — flow.md's state model rewritten around the session clock; the zero-code/advanced guides, plugin registry, glossary, and protocol pages purged of priority-era wording; the create-plugin and static-audit skills rewritten against current contracts; non-project drafts pruned from
docs/per DOCS_STRATEGY; scaffolding templates modernized; "GalGame" naming unified to stage mode.
Removed
scheduleByPriorityand the numeric priority scheduler — stage + DAG is the only scheduling authority.- Reserved trigger types —
conditional/error-retryare gone from the trigger enum (manifests declaring them fail to load), together with their five dead code remnants and the orphancondition/maxRetryCountfields. - Dead session-plan types — the never-consumed
SessionExecutionPlan/ExecutionEdge/SessionGate/ResolvedRuntimefamily.
Fixed
needs(scope: session)is real now — enforced as a positive gate in setup selection and rejected on any non-setupstage, closing the accepted-but-inert declaration trap.- Discovery advert completeness —
inputInjectKindsderives from the inject union (it had silently omittedruntime-export); trigger types were already schema-derived. - Scaffolding correctness —
create-plugin.jsemitted aninput.injectentry without the requiredkinddiscriminator (generated plugins failed validation); the plugin-with-tools template still taughtpriority+scheduled interval: 1. test-runtime --ignore-upstreamsalso strips theneedsdeclaration, not just the legacy alias.
0.0.17 - 2026-07-21
Trust-boundary and commit-integrity release, plus a smoother story-mode opening. A consolidated full-repository audit (2026-07-20) drove three remediation batches: plugin identity and community-code approval are now enforced end to end, a turn's commit outcome is authoritative everywhere, and single-connection stores serialize writes against open transactions. The GalGame stage now pre-warms its art during pre-game and appears as soon as the player submits the opening form. Toolchain moved to pnpm 11 / TypeScript 7 / Vite 8 / Electron 43, and the runtime baseline is Node 26.
Added
- Stage media preload — known scene backdrops and character sprites/avatars are warmed into the browser media cache as soon as a session opens (world-package media is already in the MediaStore at creation), so the opening turn paints them straight from cache instead of downloading after the pre-game turn ends.
- Early stage entry — the visual stage mounts the moment the player submits the opening (character-creation) form, showing the world hero backdrop and a thinking indicator until the narrator's first
scene.setswaps in the real scene art. Previously the player stared at the chat stream until pre-game fully completed. turn_results.commit_status+origin— every execution artifact records whether its proposals actually committed (pending→committed/failed; a lingeringpendingis a crash signature) and which path produced it (player/manual/follower/recursivewithparent_turn_id), sosession.turnCountcounts committed player turns only.proposal.failedSSE/trace event — a proposal commit failure surfaces to the client and withholds the completion barrier instead of reporting turn success.
Security
- Canonical plugin identity — install derives one canonical ID from the manifest root name; the scoped-npm impersonation bypass (e.g.
@covel/plugin-narratorclaiming the builtinnarrator) returns 409, and boot discovery hard-fails a dir/frontmatter identity mismatch. - Two-phase community approval enforced exactly — grants are exact-action only (a prefix match previously let any grant unlock everything); community runtime dispatch, declarative RPC handlers, and entry imports all walk
covel:plugin-server-codefirst, then the specific grant. - Executor-side tool authorization — the agent loop passes the runtime's exact authorized tool set and the executor rejects out-of-set names (checked on the final name, after overrides and PreToolUse replacement); local tools fail closed without context.
- Framework-owned identity is frozen — tool-carried proposals are rebound to the executing runtime's session/turn/source before commit;
recursiveCalltakes a delta (no session/turn/origin override) and returns a result that cannot fireturn.completed; PostRuntime replacement keeps manifest identity; PreStateCommit replacement is payload-only; PreSchedule replacement is filter-only. - Function-runtime capability revocation — a timed-out or aborted handler loses every capability (store, pluginData, media, images, speech, gateway, logger, assetProgress, recursiveCall) the moment the race settles, so a detached handler cannot write after the session lock releases.
- Plugin-data trust gaps closed — the reserved
_namespace (_jobs,_logs) and core-plugin namespaces are rejected on every plugin-controlled write path (REST, commit handlers, function-runtime writer);world-initno longer seeds a new session by copying another session's player-writable plugin-data. - Prompt-injection surface reduction — player input never rides the system prompt (
{{ player.message }}interpolation removed from narrator bodies); inject blocks are interpolated exactly once so escaped{{ … }}in player/model data cannot expand; core-memory blocks are XML-escaped with validated labels; compacted history summaries are demoted from system messages to escaped data envelopes.
Changed
- Toolchain baseline — pnpm 11.9, TypeScript 7.0, Vite 8 (rolldown), Vitest 4.1, Electron 43, Hono 4.12, Undici 8.6; Radix umbrella replaced with per-package imports. Node runtime pins moved 22 → 26 (engines
>=26, mise, CI, Docker base image digest-pinned to26.5.0-alpine3.24). - Dialogue-mode parity for tracking runtimes — character-tracker, npc-graph extractor, and codex discover the narrative engine by capability instead of naming
narrator, so character state and the relationship graph update in chat mode too. start_sessionwith an empty plugin set returns 400 instead of silently activating every registered plugin (including community and mutually-exclusive ones) for the life of the session.- Comment/doc hygiene — audit/spec/PR tracer IDs swept out of code comments and test names; docs corrected where they had drifted from the implementation (transactions boundary, turnCount semantics, postHistory scoping, emit-event dedup).
Fixed
- Commit outcome is authoritative — synchronous plugin-rpc returns 500
turn-commit-failedon an uncommitted turn; background jobs settle failed and schedule no follower onto rolled-back state; nestedrecursiveCallproposals bubble to the top-level barrier instead of being dropped; a failed best-effort auto-snapshot no longer fails an already-committed turn. - Turn accounting — a scoped
retry_runtimeno longer double-advances the turn counter; a fork's inherited count is preserved; the counter advances only after commit settles. - SSE isolation and ordering — the event-bus subscription is established inside the session lock (a queued action can no longer receive the previous execution's events under its own envelope); post-commit fan-out is buffered until the enclosing transaction commits; stream writes go through one serial queue.
- Single-connection store writes serialize against open transactions — on SQLite/Memory backends an unrelated write issued while a transaction was open used to join it (and vanish on rollback); a per-connection write gate now queues outside writes, covering the vector and mirror-media stores sharing the connection.
- World-data write atomicity —
sync-dimensionsruns under one session lock + transaction;sync-datare-checks content hashes inside the transaction (409 on drift) and defers media deletion to after commit so an abort cannot leave committed references to deleted files; media materialization compensates partial failures. - NPC graph runs on a real turn clock — relationship edges are versioned (changed relations close the open version and open a new one with authoritative
validAt/invalidAt),firstSeen/lastSeenno longer derive from row counts, an unknown turn can no longer rewindlastSeenTurn, and superseded/duplicate open edges are healed and filtered from retrieval. - Event-runtime throttling restored — fan-out now feeds real per-runtime trigger history, so
maxTriggerCount/cooldownTurnsbind again; fan-out also respects the realpreGameCompletedset so a finished Pre-Game runtime is not resurrected. - Prompt budget enforcement for tool-using agents — pruning is tool-pair-aware (no orphaned
toolmessages), which lifts the exclusion that had exempted every main agent from the hard budget; working memory is capped at render time and its entry quota enforced on both write paths. - Read-your-own-write for plugin data —
get/listoverlay the calling plugin's own uncommitted proposals from the current tool loop. - npc-graph / retry / snapshot / localization follow-ups — adjacency index prunes closed edge ids; schema-declared runtimes get a correct completion preamble instead of being told to call a tool they don't have; the framework locale rule is scoped to natural-language content so machine values (enums, ids, topics) stay verbatim; localized manifests are verified field-by-field against the canonical manifest.
0.0.16 - 2026-07-17
Performance, correctness, and reliability release. Context budgets are now derived from real model capabilities, per-turn history reads are bounded so long sessions stay fast, and a repository-wide six-dimension audit closed a batch of dead code, stale-doc, logging, and cross-backend-consistency gaps. New capability: deferred tool loading (tools.defer) lets runtimes with large tool whitelists keep the prompt small and let the model search tools on demand. Reliability fixes harden the shutdown drain, background-job sweep, event tail-frame delivery, and hosted-tier owner-token enforcement.
Added
- Deferred tool loading (
tools.defer) — a runtime may defer part or all of its tool whitelist; deferred tools stay registered and authorized but are withheld from the initial LLM tool list, and the framework injects asearch-toolstool that ranks the deferred pool with a zero-dependency BM25 (CJK-aware) and activates matches for the rest of the turn. Modeled on openai/codex'stool_search. See docs/v2/reference/tools.md. - Capability-driven context budgets — compaction thresholds and prompt budgets are derived per model from detected token limits, plus per-model USD cost estimation on the debug Cost view.
- Configurable chat window cap (
ui.chatMessageWindow, default 2000) — live message appends are bounded in memory; dropped rows remain re-fetchable via the existing scroll-up path.
Changed
- Bounded per-turn history reads —
loadTurnSessionStateno longer full-reads the wholeturn_messageslog every turn. NewDataStoremethodslistUncompactedTurnMessages,listTurnMessagesAfter(forward keyset), andgetTurnMessageStats(grouped aggregate) keep reads bounded as a session grows; the semantic-memory recall ingestion walks the log by cursor instead of re-reading and re-sorting it. - IndexedDB
deleteSessioncascade now derives fromSESSION_SCOPED_TABLES, matching the SQL/Memory backends — a newly registered session table can no longer silently leak rows on the browser backend. - Removed two zero-caller extension seams — the
findToolClienttool-client resolver and the per-runtimegetConfigFn/ctx.config/{{ config.* }}chain (always empty in production) were deleted end to end. - Framework/plugin isolation and docs — removed a write-only
blockSchemaschain, retired an unread env-registry entry, closed authoring-contract drift, and synced all reference/guide docs to the current architecture.
Fixed
- SSRF self-tier core-provider path — the DNS-pinning guard rejected provider hosts on machines running a TUN proxy (Clash/mihomo/sing-box/Surge fake-IP maps every domain into a private/benchmark range) or a LAN Ollama endpoint. The
selftier now accepts any resolver answer on the user's own configuredbaseUrl(socket still pinned); pluginctx.http, IP-literal URLs, and hosted tiers stay strict. - Settings persistence on the REST-desktop tier — self-host setups without the Electron IPC bridge now persist settings to
~/.covel/settings.jsoninstead of silently falling back tolocalStorage; the settings REST backend surfaces failed writes instead of swallowing them. - Plugin-panel re-render storm — plugin-data store snapshots are scoped per
(pluginId, namespace), so an unrelated plugin's data write no longer re-renders every active panel. - Malformed JSON bodies now return
400 invalid_json_bodyinstead of a generic 500 across ~13 API routes (sharedreadJsonBodyhelper). - Shutdown drain flushes in-flight turns before closing the store; background-job sweep periodically reclaims orphaned pending jobs; event delivery detects lost tail frames via a transport heartbeat.
- Hosted-tier auth hardening — owner-token hash stripped from responses, event injection gated, owner guard enforced on the resume
DELETEsuspension route; desktop IPC sender-origin checks extended to the remaining channels. - Observability & logging — turn-executor and shutdown paths keep error stacks instead of
err.messageonly, the Data Explorer and model-db refresh catch paths log, and[component]prefixes are consistent.
0.0.15 - 2026-07-16
Security and reliability hardening release. A full-repository audit and two follow-up remediation rounds closed the credential, trust-boundary, transaction-consistency, and event-delivery gaps that made the previous build unsafe to expose on a public, multi-user, or multi-instance deployment. Local single-user self/desktop play is unchanged by default; the new hosted-tier controls are opt-in via DEPLOYMENT_TIER.
Security
- Provider keys stay bound to their trusted origin. Server/platform API keys now flow to the gateway as
envApiKeys, separate from request-suppliedX-Provider-Keys; an env key attaches only when the resolvedbaseUrlorigin matches trusted config (llm.toml/ registered provider defaults). A request-scoped custom preset that redirects a provider to another origin receives no env key and no trusted default headers, closing the request-level key-exfiltration chain. - Per-request slot overlays are isolated. Custom presets register under a request-derived scoped id, so two concurrent requests using the same provider name with different base URLs can no longer share a registration — a victim's browser key can never be sent to an origin another in-flight request registered.
- Core provider HTTP resolves DNS through a pinning dispatcher.
postJson/getJson/postFormData(not just the pluginctx.httphelper) validate every A/AAAA answer at connect time and reject private/link-local/metadata addresses, closing the string-check-to-connect DNS-rebinding gap. - Hosted deployments gate every session and global route. A per-session owner token (minted at create, hash-persisted, returned once) guards all session-scoped routes; hosted (
demo/commercial) tiers additionally require an operator credential to create/list sessions and reach global admin/model/world routes. The server binds127.0.0.1by default (COVEL_BIND_HOSTopts into a public interface). A startup posture check fails closed on a hosted tier missing its media secret, CORS origin, or operator token.self/desktop/dev tiers remain a strict no-op. - Community plugin code is import-gated behind two-phase approval. A community (third-party) plugin's server code (
entry/ handler / hook / wire / runtime JS) is never imported until an explicitcovel:plugin-server-codegrant is approved for the session; the specific action then requires its own approval. Legacyhooks:handlers defer their import behind the same gate, the community entry store is default-deny (writes flow through proposals), and a timed-out agent guard has its write capabilities revoked so it cannot mutate a later turn. - Desktop hardening. Electron blocks main-frame navigation off the sidecar origin and validates the sender frame on secret-returning IPC; ZIP imports enforce entry-count / total-size / per-entry / ratio limits and no longer accept renderer-supplied arbitrary paths.
- Supply chain and logging. Pinned
only-allowand Docker image digests; Postgres binds host loopback; media and session tokens are redacted from error logs; GitHub Actions pinned to commit SHAs.
Added
- Operator Access settings pane to save / clear / show / hide the hosted operator token, reloading session and world data on change.
- Pull-request / push CI running install →
pnpm lint→pnpm deps:check(dependency hygiene) → tests → build → i18n/plugin checks;DEPLOYMENT_TIERis parsed against an explicit enum and fails closed to the most restrictive tier on an unknown value.
Changed
- Community plugins seeded by a world now require explicit approval on every tier. A third-party (
community-trust) plugin listed in a world manifest is no longer auto-activated and auto-loaded on first schedule — it is dropped from a session's active set at creation and must be explicitly enabled and approved (two phases: load the plugin's server code, then the specific action) before any of its code executes. This now applies on all deployment tiers, including localself/desktop, matching the documented community trust model (deferredimport()until the user approves). Bundled sample worlds are unaffected — their plugins arebuiltin/officialand still auto-load at boot. Migration: if you author a world that references a third-party community plugin, players must enable and approve it in-session; a previously-working self-tier world that relied on such a plugin auto-loading will start with it inactive until approved. - Turn side effects are strictly post-commit. Externally-visible "committed" SSE/trace broadcasts,
PostStateCommithooks,turn.completed, and post-turn memory/vector ingestion now run only after the commit transaction resolves, so a rolled-back batch emits no ghost events and never writes memory for a failed turn; a singletraceIdthreads the whole turn. - Snapshots are checkpoint-throttled and paginated (
COVEL_SNAPSHOT_INTERVAL_TURNS, default 5), with a store-level metadata projection + keyset pagination that never deserializes payloads. - Streaming text is decoupled from message history into an external store, so per-delta updates are O(1) and the chat grouping memo no longer rebuilds per token; the IDB backend and debug route move out of the main web chunk.
Fixed
- EventBus delivery is bounded and gap-aware. A fixed-capacity ring buffer with LRU/idle eviction (active subscribers pinned),
${epoch}:${seq}non-reusable ids, replay-gap detection that drives asystem.reset→ client re-hydrate, a per-session ordered cross-pod outbox over PGLISTEN/NOTIFY, and leak-free SSE cleanup with a per-session connection cap and bounded write backpressure. - The compactor cascades past the first compaction — the window starts after the last boundary and tokens are estimated from the effective prompt view, so long sessions keep compacting instead of eventually overflowing the model context.
- Graceful shutdown drains watchers, event bus, store, and PG lock pool; background jobs get a startup recovery sweep and a concurrency cap; the PG advisory-lock deadline now covers pool checkout.
- Turn hot-path reads reduced (working memory deduped 3→1 per turn; touched-only transaction snapshots), IDB keyset pagination with an atomic v11→v12 metadata migration, and session-owner coverage extended to
approvals/media/ world-sync /ui-specs.
0.0.14 - 2026-07-13
The agent-core release. Server-side plugin registration converges into a single entry factory (covel.registerTool / on / registerRpc / registerWires), players can steer or abort a turn while the LLM is still streaming, and a full-codebase audit hardened the turn/snapshot/fork consistency boundaries: proposal commits are now atomic under the session lock, auto snapshots capture post-commit state, forks restore session lifecycle from the snapshot itself, and outbound plugin fetches pin DNS resolution against rebinding.
Added
- Unified PluginAPI (
entry). The four scattered server-side registration surfaces (local tools, hooks, RPC actions, media wires) converge into one factory declared via the newentryPLUGIN.md field —export default function (covel) { ... }withcovel.registerTool / on / registerRpc / registerWiresandcovel.toolkit. Agent runtimes declare LLM tool visibility with the newtools.pluginname list. All 8 bundled plugins with server-side registrations migrated; legacy fields keep working for one release cycle with a per-plugin deprecation warning, and community entries still defer to activation (trust gating unchanged). - Mid-turn player steering + abort.
POST /api/sessions/:id/steermerges queued player interjections into story runtimes' next LLM step (persisted to history);POST /api/sessions/:id/abortcuts the in-flight LLM stream immediately, is non-retriable, bypasses the streaming salvage path (no partial narrative is ever committed), and surfaces asabortReason: "aborted-by-player"on the existingexecution.completedevent. Both return 409 when no turn is active. The web composer stays usable during a turn — submit steers, a stop button aborts. - Snapshot payload schemaVersion 2. Snapshots now capture session lifecycle and runtime configuration (
status,turnCount,preGameCompleted,locale,activePlugins,presetId,runtimeModelOverrides), so forking an old snapshot restores the session as it was at capture time instead of inheriting the parent's current scheduling state. V1 snapshots remain readable and fork with the previous parent-fallback behaviour.
Changed
- Agent loop layering. All how-to-run derivation (tool defs, schema gate, model override chain, streaming gate, step/retry budgets,
requireToolUse,acceptsSteering) converges intobuildAgentLoopPolicy(); the loop's single delta outlet is extracted intocreateDeltaForwarder. The loop body is control flow only and independently instantiable — pinned by a direct loop-core test suite (trace/delta sequences, runtime-done stripping, maxSteps, streaming gates, steer/abort/salvage-bypass).
Fixed
- Turn commits are atomic under the session lock. Player-input persistence, turn execution, proposal commit,
turnCountsync and the auto snapshot now run inside a singlesessionLock.withLockscope, so an overlapping same-session action can no longer read pre-commit state (stale-read / lost-update). A bounded PG advisory-lock acquire timeout surfaces as a coded, retryable 503 instead of a generic 500. - Auto snapshots capture post-commit state. Snapshot capture moved out of the turn finalizer (which ran before proposals committed) into the server pipeline after commit — forked sessions no longer inherit a turn's dialogue while missing that same turn's state, character and plugin-data writes.
- Manual snapshots and forks share the session lock, so payload reads can no longer observe a mixed point-in-time while a turn is committing.
- Plugin uninstall honours the install privilege boundary.
DELETE /api/plugins/:id(which recursively deletes the plugin directory) now sits behind the same production opt-in / desktop bearer-token guard as install, instead of being callable by anyone who can reach the API. - Outbound plugin fetches pin DNS resolution. SSRF validation previously only checked the literal hostname; a public name resolving to a private/metadata address passed.
fetchWithRetrynow resolves and policy-checks addresses per attempt and pins the connection to the validated address via an undici dispatcher (DNS-rebinding defence). - PR #18 review hardening. Plugin tool-access allowlists only grant ownership after successful registration (tool-name collisions are rejected instead of leaving a dangling grant); the active turn registers inside the session lock so a queued action can no longer steal the steer/abort target; the web client clears its delta buffer and streaming placeholder on player abort (no ghost message).
0.0.13 - 2026-07-10
The Windows release. The dev environment (spawn shims, plugin loading, supervised pnpm dev) now works on Windows, and CI builds Windows installers alongside the macOS bundles on every tag. Public web-only (browser IndexedDB) deployments are hardened: the shared session listing is hidden and the Render blueprint pins the memory backend explicitly.
Added
- Windows desktop CI.
release.ymlgains abuild-electron-winjob (windows-latest): full desktop build with Electron-ABI native rebuild, staged-server smoke under Electron's Node mode,electron-builder --win(NSIS + portable, x64, unsigned until a cert is configured), and.exeartefacts attached to GitHub Releases alongside the macOS bundles. Windows targets are x64-only: the staged server ships a better-sqlite3 rebuilt for the build host's arch, so an arm64 installer from an x64 runner would carry an addon the arm64 sidecar cannot load — Windows-on-ARM runs the x64 installer via emulation.
Fixed
- Windows dev environment. Dev scripts spawn through
cross-spawn(resolvespnpm.cmd-style shims with correct cmd.exe quoting); plugin/runtime dynamic imports of absolute paths go throughpathToFileURLso plugin loading works on Windows;pnpm devlaunches web + server directly with supervised teardown;--env-file-if-existstolerates a missing.env(raises the minimum Node to 22.9). - Desktop staging smoke now exercises the Electron ABI. The native rebuild runs before the smoke test and the staged server boots under
ELECTRON_RUN_AS_NODE; a missing Electron binary fails the build instead of silently downgrading the check (COVEL_SMOKE_HOST_NODE=1opts into the weaker host-Node smoke). - Electron runtime binary is materialised explicitly. electron@42 removed its postinstall hook, so
pnpm installno longer downloads the binary intonode_modules/electron/dist(whitelisting inonlyBuiltDependencieshas nothing to run). The desktop build and dev shell now runensure-electron.mjsfirst, which invokes electron'sinstall.jswhen the binary is missing — without this, every desktop CI build died at the staging smoke on both macOS and Windows. - Public web-only hosting hardening. On memory-backend production deploys without debug routes,
GET /api/sessionreturns an empty list — server-side sessions there are transient sync copies of browser-IndexedDB data, and the only callers of a shared listing would be other players.render.yamlpinsSTORE_BACKEND=memoryexplicitly (the sqlite default would pool every player's data in one shared DB), adds a health check, and disables the debug page; the Docker image regains the workspace manifests (settings,plugin-handlers-utils, and four plugins) missing from its dependency-install stage.
0.0.12 - 2026-07-06
The media-pipeline release. Speech joins images as a first-class framework primitive: ctx.speech gives function runtimes a unified TTS/STT surface with the same wire-selection, dedupe, and MediaStore-persistence guarantees as ctx.images, and every media modality (image / speech / transcription) now routes through pluggable per-modality wire registries. Plugins can ship vendor wires declaratively via the new PLUGIN.md wires field — supporting a new provider no longer requires touching bundled code.
Added
ctx.speech— unified TTS/STT for function runtimes.generate()synthesizes speech, dedupes onsha256(presetId, text, voice, format), and persists through MediaStore;transcribe()accepts aMediaRefor raw bytes and returns plain text. Assembled under the same conditions asctx.images; the plugin gateway and trace layer gainsynthesizeSpeech/transcribeAudiopassthroughs.- Plugin-declared media wires (PLUGIN.md
wiresfield). Plugins register image / speech / transcription wires declaratively; ids are namespaced<pluginId>/<wireId>and loading is trust-gated. Slots select wires viaproviderRequestMetadata.imageWire|speechWire|transcriptionWireinllm.toml. - Manifest diagnostics at bootstrap. A declared capability tag sitting one edit away from a framework-known one now warns (a misspelled tag was previously just silently never discovered), as does a plugin whose frontmatter name root differs from its directory name (a mismatch silently denies the plugin its own local tools).
Changed
- Speech/transcription move from provider adapters to pluggable wires (builtin
openai-speech/openai-transcription), completing the per-modality wire registry started with images in 0.0.11. - Plugin handlers read plugin data exclusively through
ctx.pluginData— no more store-shape sniffing; trusted and community runtimes go through the identical scoped path. Emptyrelations: {}frontmatter dropped across all bundled plugins; runtime imports moved from devDependencies to dependencies where misfiled; world-init tests relocated totests/, guide + pregame gain handler tests. - Web session view slimmed.
game-viewreads session state from the session store instead of ~20 pass-through props; a shareduseSettingsDialoghook replaces duplicated dialog state; UI primitives trimmed to the variants actually used.
Fixed
video_generationmodels derive a video output modality instead of being misclassified as text.- Commit handlers reject
state.patch/event.emitproposals with an empty table/field/topic instead of silently landing writes underdefault/unknown(JS plugins bypass the type layer, so the gate must live at commit time). POST /api/media(10 req/min) and sessionplugin-rpc(30 req/min) are rate-limited per IP — runtime-mode RPC dispatch runs a full LLM turn, the same cost class asPOST /api/actions.
0.0.11 - 2026-07-04
The visual-novel release. A full-screen GalGame stage mode — scene backdrops, character sprites, typewriter dialog, choice overlays — built on three new framework layers: a unified event-emission layer (events contracts + emit-event), a first-class image-generation pipeline (ctx.images + pluggable wires), and the scene-stage plugin that resolves narrative locations into stage art and generates missing backdrops on demand, mid-session. Haruka Academy ships the full asset set and plays as a visual novel out of the box.
Added
- Stage mode (
viewMode: "stage"). A full-screen visual-novel view for Playing turns: crossfading scene backdrop, bottom-anchored character sprites with active-speaker highlight, a delta-driven typewriter dialog with paragraph pauses, a classic centered choice overlay (interaction choices + scene-prompts phrases + free input), a HUD (scene badge, history drawer, auto-play, immersive toggle), and a player-clean history drawer reusingChatMessages. Worlds pick their default via the newworld.yamldefaultViewMode: stage | parsed; players can switch any time. Sprite stationing is sticky — salience decides who is on stage and who is highlighted, never where anyone stands — and sprites render inside equal-width lanes, so occlusion is impossible by construction. - Unified event layer. Plugin runtimes declare event contracts in frontmatter (
events: [{topic, schema, description, advertise}]); emitting agents declareadvertiseEvents: trueand call the new builtinemit-eventtool, whose payloads are schema-validated against the declaring plugin's contract with per-turn topic dedupe. A per-session event directory aggregates active plugins' contracts into the prompt (<available-events>), and deferred event followers are scheduled once per turn on the main loop.narrator/chat-mode-narratoremitscene.setas the reference implementation. - Framework image pipeline (
ctx.images/gateway.generateImage). One primitive for plugin image generation: wire selection (builtinopenai-imagesanddashscope-wansubmit+poll, extensible viaregisterImageWire()), MediaStore persistence, and promptHash dedupe all handled by the framework — handlers never touch bytes or provider credentials. scene-stageplugin. Resolvesscene.set(location + day/night) against the world scene registry — exact, then fuzzy, then session-generated matches — writesstage/currentfor the stage backdrop, and queues background generation for unmatched locations behind player-tunable gates (autoGenerateScenes,maxGeneratedScenes). Night variants lazy-generate on first use and reuse the day image's visual hint.- Haruka Academy visual-novel asset set. Ten scene backdrops (5 locations × day/night) plus regenerated transparent-background portraits, imported via new
worldDatamedia sources (to: media+indexTo+key: filename) and thescenes.registry.json/presence.jsonregistries;scripts/generate-scenes.mjs+scripts/emit-scenes.mjsregenerate them fromscenes.jsonspecs. requireToolUsemanifest gate. An agent runtime whose whole job is a tool call gets one corrective retry (locale-aware message) when it drifts into prose without calling anything;scene-promptsuses it.
Changed
- Media store simplified. The unused S3 backend and its SQLite/PG metadata adapters are gone; media stores gain a shared
listByMetadatafilter. The imperativebeginTx/commitTx/rollbackTxAPI and the emptyplugin_configstable are removed (all writes go throughwithTransaction; existing databases are unaffected). - ai-provider slimmed. Dead config loader, token estimator, and protocol-registry indirection removed; model capability resolution unchanged.
- Desktop auto-updater removed — releases install via the dmg; update checks return in a later cycle.
- README rewritten around stage mode with a fresh 6×-speed demo gif and play-mode stills; the
create-world/create-pluginskills now document the VN feature set (defaultViewMode, asset pipeline, events contracts,ctx.images).
Fixed
plugin-data.changedSSE now fires after transaction commit (and never on rollback), so panels re-render exactly when data is durable.- worldData sources targeting a plugin the player deselected are skipped with a warning instead of failing session creation with a 500;
indexTotargets skip only the index writes. - Stage polish batch: sprites no longer drift when the speaker or scene changes (sticky stations + lane geometry), stale
getSessionresponses can't corrupt state after a session switch, the typewriter survives same-text turns and stream-end races, artless speakers keep a name-card on stage, execution errors surface on stage with retry, and choice overlays no longer cover sprites. - A stuck "generating…" backdrop retries when the scene re-emits after a failed generation; DashScope image tasks that are CANCELED/UNKNOWN fail fast instead of polling to timeout.
- Duplicate deferred event followers are deduped per turn; enum values render in the event catalog; image cache hits stamp per-call metadata.
0.0.10 - 2026-06-29
A plugin-configuration pass: worlds can now preset any plugin's player-tunable settings and declare genre-specific memory dimensions, the player's per-plugin settings finally reach the scheduled turn loop, and the plugin config layer is consolidated onto a single declaration with server-enforced constraints. Behavior-unchanged for worlds and plugins that don't adopt the new fields.
Added
- World-preset plugin settings (
pluginSettings). Aworld.yamlcan now declare per-plugin defaults for any plugin'suserSettings, keyedpluginId → settingKey → value. They form the middle layer of a three-tier resolution chain — player override → world default → manifest default — merged at the turn boundary intoTurnInput.userSettingsand consumed by agent{{ userSettings.* }}templates, guards, and hooks. A dialogue world can ship "70% dialogue, short replies" as its default while players keep the freedom to override. Seedocs/v2/reference/world-data.md. - World-declared core-memory blocks (
memoryBlocks). A world can add genre-specific memory dimensions (a detective world'sclues/suspects, a business sim'sdeals/rivals) without forking a plugin — same shape as a plugin'smemoryBlocks. The memory system resolves the block schema per session, merging a session's world blocks onto the global plugin blocks (base wins on label collision; the world only adds new labels), so the extra dimensions render and extract only for the worlds that declare them. Closes the long-documented "(or by a world package)" gap. integer/slideruser-setting types, and a consolidated design-principles page (docs/v2/internals/design-principles.md) that roots the framework's "kernel provides primitives, plugins carry gameplay" contract, the agent / function / composition writing styles, and the plug-vs-appliance test.- Character portraits + presence wiring for the flagship worlds. Both worlds ship a curated portrait set (mistport fog-noir, haruka GalGame-style), generated via
scripts/generate-portraits.mjs+scripts/emit-presence.mjsand wired to thecharacter-presenceplugin throughmedia+presenceworld-data sources (content-addressed by sha256) — portraits render in the right-panel character card and as dialogue sprites. The two worlds also gained tuned plugin defaults (haruka:activeSpeakerCount,npc-graph; mistport: acost-gatebudget guardrail). Prompt spec indocs/v2/worlds/portraits.md; shipping contract indocs/v2/reference/world-data.md. - Hot-reload
llm.tomlwithout restarting. Settings → LLM gains a Reload config button (POST /api/llm-config/reload) that re-readsllm.tomland applies it to the live gateway in place — the provider / preset / slot registries are reconfigured rather than rebuilt, so the gateway and every adapter pick up added/removed slots without a restart. A malformedllm.toml(e.g. a key with=and no value) no longer fails silently: the file still falls back to the built-in default, but the parse error is now surfaced via the newerrorfield onGET /api/llm-configand a red banner in Settings — instead of every plugin's slot just showing as "missing" with no explanation. On desktop the reload endpoint is gated by the same one-time REST token as other config writes. Seedocs/v2/reference/desktop-config.md. - Character portraits show as avatar badges in the character list. Each row in the char-creator character panel now leads with the character's portrait as a small avatar badge, via a new generic
CharacterAvatarjson-render component. It reads presence records from a source plugin/namespace named in the spec (char-creator wires it tocharacter-presence/presence) — keeping the framework component free of any hard-coded plugin id, mirroring howImageGallerytakes itspluginIdby prop. Matching tolerates the<sessionId>-<characterId>ids the character list uses against the barecharacterIdthe presence records key on. Characters without a portrait — and entire worlds without presence data — render exactly as before: the badge simply doesn't appear. - Worlds can declare character attributes with i18n labels (
characterAttributes).AttributeDefinition.name/descriptionnow accept anI18nText(plain string or{ "zh-CN": …, "en-US": … }record), and aworld.yamlcan ship its owncharacterAttributesarray.world-init's guard writes it verbatim as the session'scharacter-attributesschema — and does so authoritatively, ahead of cross-session reuse, so editing the declaration takes effect on new sessions instead of inheriting an older session's stale/derived schema. The right-panel character card resolves each label to the UI locale (resolveI18nText), and the prompt-injected<world-schema>is deep-resolved to one language too (resolveI18nDeep), so a custom field like Haruka'saffectionshows as 好感度 / Affection instead of a rawaffectionkey under "其他". Both flagship worlds now declare their cast fields (mistport: faction / role / tideSense / fogRot / …; haruka: club / affection / trust / …) with bilingual labels. Worlds that declare nothing are unchanged — the guard still derives generic attributes from dimensions, then falls back to the LLM. Seedocs/v2/reference/world-data.mdanddocs/v2/reference/plugins.md. - Click-to-enlarge for portraits, avatars, and generated images. The image-generation gallery's preview was extracted into a generic
MediaPreviewDialog. The json-renderImagecomponent gains azoomprop, the character-portrait gallery and the character-list avatar badges opt into it, and the image gallery now delegates to the same dialog — one enlarge + download surface everywhere. The avatar badge stops its click from also toggling the surrounding collapsible card. TheImagecomponent also gains aframedprop (rounded bordered card + hover) so the portrait gallery's thumbnails match the image-generation gallery's cards — the two read as one family. The enlarge view is a tight lightbox that hugs the image (w-auto) rather than a wide dialog with the image floating in empty space. branch-replyreply-swipe revived as a real feature. The GalGame "swipe between AI replies" plugin shipped active in both default worlds but was a permanently-invisible dead feature — a bootstrap deadlock where the only candidate-writer lived behind a button inside a block that never rendered. It is now a working auto-seeded swipe: after each story turn it captures the narrator's reply as the original (engine-agnostic — discovered via the narrative-output contract, no hardcodednarrator/chat-mode-narratorid), and a Regenerate control produces genuine LLM-generated alternative phrasings (fast slot, session locale) the player can compare and Accept to rewrite prompt history. The original is never re-rendered as a card, so a reply never appears twice; the old English deterministic filler is gone.
Changed
- One plugin-config declaration.
userSettingsis now the single source for player-tunable plugin settings; the dead, never-consumedconfigmanifest field is removed. A stragglerPLUGIN.mdthat still declaresconfig:is stripped with a deprecation warning rather than failing to load. ThePluginUserSettingSpectype is single-sourced in@covel/shared(two divergent copies removed), and its declared constraints (min/max/options/ type) are now enforced — client-side in the SettingsStore and server-side inresolveUserSettings, so an out-of-range world or header value degrades to the manifest default instead of reaching a guard or hook. - Plugin selection consolidated into
pluginPolicy. The deprecated top-levelrequiredPlugins/recommendedPlugins/excludedPluginsnow fold (de-duplicated) intopluginPolicyat world-load time, soWorldRecord.metadatacarries plugin selection in one place. The top-level fields remain valid inworld.yamlfor back-compat. Per-turn world reads are served from a short-TTL per-worldIdcache. - Sample worlds curated to two deeply-built flagships. The bundled set is now Mistport (traditional-story / dark-fantasy mystery) and Haruka Academy (dialogue / GalGame-style), each greatly expanded and doubling as the canonical example of the new fields. Mistport gains a seven-character seed cast, a fourth faction, bilingual lore, and
clues/relics/tidesmemory blocks; Haruka grows to eight characters across five routes withrelationships/promises/rumors/festivalmemory. The redundantcloudmereandneonridgestory worlds move toworlds/_archive/(kept for reference, not loaded).
Fixed
- Player per-plugin settings now actually reach the main turn loop.
POST /api/actions(and the resume / plugin-rpc paths) never decodedX-Plugin-User-Settings, so in the scheduled loop every runtime'suserSettingscollapsed to manifest defaults — a player's UI-tuned values (chat-mode-narrator'sdialogueRatio, cost-gate's token caps) were silently ignored while only the manual plugin-rpc path honored them. The main route now decodes the header and merges it with the world defaults. - The settings header no longer masks world defaults.
X-Plugin-User-Settingswas built from every registered plugin setting (store.get()returns the manifest default for untouched keys), so it always carried a full bucket of defaults that, sent as "player overrides," overrode the new worldpluginSettingslayer. It now carries only the keys a player explicitly set (store.has()). - Archived bundled worlds no longer linger in existing users' databases.
seedWorldsonly ever upserts, so a sample world removed from the bundle (e.g.cloudmere/neonridge) stayed seeded in every existing user's DB and kept showing in the world list. The server now runs a reconciliation pass after seeding that drops file-seeded worlds no longer present in any world source. Three safety rails keep it from ever removing real data: it only touchesmetadata.source === "file"worlds (AI-generatedgenerated/generated-fileworlds are never touched), it keeps any stale world that still has saved sessions (warned, never silently deleted), and it skips entirely when nothing seeded so a transient load failure can't wipe worlds. Seedocs/v2/reference/world-data.md. - Plugin provider-slot overrides are honored in the prep screen. A function-runtime plugin (e.g. image generation) names its provider slot through the
modelPresetIduserSetting. The session-prep row read only that setting's manifest default, so it always displayed the default slot — flagging it red as "missing [covel.<default>]" even when the player had a perfectly good slot of the same kind configured, with no way to point the plugin at it short of editing Settings → Plugins. The row now reflects the player's effective value and offers an inline slot picker; choosing a configured slot clears the warning, and the override reaches the function runtime via the (now-honored)X-Plugin-User-Settingsheader. Seedocs/v2/reference/desktop-config.md. - Creating a world that ships portraits no longer 500s on SQLite (
database is locked).POST /api/sessionswrapscreateSession+ world-data import in a singlewithTransaction. WithSTORE_BACKEND=sqlite, the mirror media store opened a second connection to the samecovel.db, so materializing the world's bundled portraits inside that open write transaction deadlocked against the transaction's own lock —SQLITE_BUSY, surfaced as a generic 500 after the 5s busy-timeout. The SQLite DataStore and its mirror media store now share one refcounted connection per file, so media writes join the open transaction and commit atomically with the session (the intended behavior) instead of fighting it. Worlds without media were unaffected; this only bit the new portrait-carrying flagship worlds. - Imported character portraits now actually render. The portrait/presence wiring stored the data (sha256-addressed images + presence records), but the
character-presenceright panel only dumped the raw presence JSON — a wall of sha256 ids with no<img>anywhere, so players couldn't see the art. The panel now renders a portrait gallery at the top (repeatover the presence entries → oneImageper character, captioned bydisplayName), and is surfaced as its own Character Portraits (角色立绘) tab right after the character list — instead of being buried as one of four providers inside the sharedchat-mode("对话") group tab, where it was effectively unfindable. Worlds without portraits degrade cleanly: an empty presence list shows no gallery, and a character missing an avatar falls back to the built-in "no image" placeholder tile — no broken images, no errors. The panel spec re-materializes onGET /api/ui-specs, so existing sessions pick it up on the next reload without starting over. The player-facing tab no longer exposes the world-author import form (which made you hand-enter an image's sha256 id / MIME / byte size). It's now a customPortraitGallerycomponent: each portrait is a framed click-to-enlarge thumbnail with a hover-to-replace action — the player just picks an image file, which uploads via the newPOST /api/media?sessionId=(session-owned, content-addressed) and is written back as the character's presence through the plugin's own runtime. No internal fields, no JSON. World authors still bulk-import portraits via the world package (presence+mediasources). - Desktop media (portraits, generated images) no longer fails to load. The packaged desktop sidecar runs with
NODE_ENV=production, where the server refuses to mint media-access tokens from an ephemeral per-process secret — but the shell only provisioned the REST bearer token, neverCOVEL_MEDIA_TOKEN_SECRET. SoGET /api/sessions/:id/media-token500'd and every<Media>(character portraits, image-gen output) showed a "media unavailable" placeholder despite the bytes being on disk. The desktop shell now generates a per-launch media-token secret and injects it (a user-setCOVEL_MEDIA_TOKEN_SECRETvia process env / a repo.envstill wins). Tokens are 5-minute TTL and re-requested by the renderer, so per-launch rotation is invisible. - Non-dialogue panels no longer hide under a "Chat Mode" tab.
character-blueprint(preset characters),living-world-rules(world rules), andplayer-identity(player voice) each declaredgroup: "chat-mode", so their panels merged into one tab literally labeled "对话" / Chat Mode in every world — jarring in a traditional-story world like Mistport, which doesn't run dialogue mode at all. They are now their own clearly-labeled tabs (预设角色/世界规则/玩家口吻); the genuinely dialogue-only panels (scene-cast/scene-prompts) keep the chat-mode group. Mistport also dropscharacter-blueprintfrom its recommended set — a dialogue-mode cast-authoring tool that overlapschar-creatorfor traditional story. - World dimensions are localized before they reach the narrator's prompt. A dimension's i18n leaves (
{ zh, en }faction names, descriptions, tone, opening scenario, …) were injected into{{ world.dimensions }}as a raw bilingual blob — wasting tokens and risking language-mixing — whilelorewas already locale-resolved at load.buildWorldContextViewnow deep-resolves the dimensions to the session locale (new sharedresolveI18nDeep), so the narrator (andworld-init/schema-gen) see one language. As a bonus,tone/openingScenarionow extract correctly when authored as i18n records (the oldtypeof === "string"check silently skipped them). - Plugin UI/UX coherence pass (multi-agent audit). A systematic review of every bundled plugin's panel ↔ context ↔ tool wiring fixed a cluster of coordination defects where the player saw one thing while the model saw another, or a control did nothing:
- Character defaults persist at the write boundary. New
mergeSchemaDefaultsmerges declared schemadefaultValues into storedfieldsin bothcreate-characterand the player-init guard, so the panel (which overlaid defaults at render), the model'sget-character, and the prompt context finally agree — previously the panel showedhp 100/100that existed in neither the record nor the prompt. scene-castactiveSpeakerCountknob works. It was declared onchat-mode-narrator(a different plugin scope), so the slider never reached the plugin that enforces cast size (hard-capped at 2) while the narrator was told a count the cast never matched. The setting moved ontoscene-cast; the narrator defers to the injected<active-cast>.living-world-rulesno longer silently drops rules. Atriggered/evolvingrule saved without keywords mapped to a keyword-gated lorebook entry that never activated, yet showed as enabled. Only keyword-bearingtriggeredrules are now keyword-gated; everything else is always-on.cost-gateaborts are visible. A hard-budget abort on the main turn emitted an emptyexecution.completedwith no signal; it now carriesabortReason(protocol + SSE + UI) so the player gets a reason instead of a silent empty turn.chat-mode-narratorstops double-injecting the active-cast and NPC-relationship context each turn (inline{{ inputs }}+input.injectboth appended it).- Player-facing surfaces de-jargoned. The raw-JSON "full import" textareas (player-identity / living-world-rules / character-blueprint) and the raw
JsonView"saved …" dumps were replaced with structured cards (a new genericEntryCardisActivebadge marks the active player voice); the deadcodexui.messagespec and the redundant raw-JSONworld-entriestab were removed. - Runtime-written strings are localized. Labels/prose written by tools/hooks bypassed the i18n gate and rendered Chinese for en players — scene-prompts / guide badge labels (now I18nText), the director preamble + pregame welcome (now
ctx.locale-resolved), andchat-mode-narrator's prompt body (addedPLUGIN.en.md).check-plugin-i18nwas extended to scan handler.jslabel/title/placeholderliterals so the class can't regress (it caught a real pregame leak). memory.character_relationshipsre-scoped to player-centric bonds so it complements rather than duplicatesnpc-graph's NPC↔NPC graph (a careful check showed a naive merge would have regressed player-relationship continuity).
- Character defaults persist at the write boundary. New
中文(备份翻译)
一次插件配置整理:世界现在能为任意插件预置玩家可调设置、声明题材专属的记忆维度,玩家的 per-plugin 设置终于进入主回合循环,插件配置层收敛为单一声明并在服务端强制约束。未采用新字段的世界与插件行为不变。
Added
- 世界预置插件设置(
pluginSettings):world.yaml现在可为任意插件的userSettings声明默认值,键为pluginId → settingKey → value。它是三层解析链的中间层——玩家覆盖 → 世界默认 → manifest 默认——在回合边界合并进TurnInput.userSettings,供 agent{{ userSettings.* }}、guard、hook 共用。对话世界能把"70% 对话、短回复"作为默认,玩家仍可覆盖。见docs/v2/reference/world-data.md。 - 世界声明核心记忆块(
memoryBlocks):世界可添加题材专属记忆维度(侦探世界的clues/suspects、商战的deals/rivals),无需 fork 插件——字段形状与插件memoryBlocks一致。记忆系统按 session 解析块 schema,把该会话所属世界的块合并到全局插件块之上(标签冲突基础块优先、世界只新增),使额外维度只在声明它的世界里渲染与抽取。兑现长期文档承诺的 "(or by a world package)"。 integer/slider用户设置类型,以及一页收敛的设计原则(docs/v2/internals/design-principles.md),确立"内核提供原语、插件承载玩法"的契约、agent / function / 组合三种写法、以及"插头 vs 电器"裁决测试。- 旗舰世界的角色立绘 + presence 接线:两个世界各自交付一套精选立绘(mistport fog-noir、haruka GalGame 风),由
scripts/generate-portraits.mjs+scripts/emit-presence.mjs生成,并通过media+presence世界数据 source(按 sha256 内容寻址)接入character-presence插件——立绘在右侧角色面板与对话立绘中显示。两个世界还预置了调优的插件默认值(haruka:activeSpeakerCount、npc-graph;mistport:cost-gate预算护栏)。提示词规范见docs/v2/worlds/portraits.md,交付契约见docs/v2/reference/world-data.md。 - 热重载
llm.toml,无需重启:Settings → LLM 新增「重载配置」按钮(POST /api/llm-config/reload),重读llm.toml并原地应用到运行中的 gateway —— provider / preset / slot registry 是重新配置而非重建,所以 gateway 与所有 adapter 立即看到新增/删除的 slot,无需重启。llm.toml写错(如某个 key 有=却无值)不再静默失败:文件仍回退内置默认,但解析错误现在通过GET /api/llm-config新增的error字段 + Settings 顶部红色提示暴露出来——而不是只让每个插件的 slot 显示「缺少」却不知为何。桌面版该接口受与其他写接口相同的一次性 REST token 保护。见docs/v2/reference/desktop-config.md。 - 角色列表里直接显示立绘头像角标:char-creator 角色面板每一行现在以角色立绘的小头像角标开头,通过新增的通用
CharacterAvatarjson-render 组件实现。它从 spec 指定的来源插件/namespace 读 presence 记录(char-creator 把它接到character-presence/presence)——框架组件里不硬编码任何插件 id,与ImageGallery用 prop 接收pluginId同理。匹配兼容角色列表用的<sessionId>-<characterId>与 presence 记录用的裸characterId。没有立绘的角色、以及完全没有 presence 数据的世界,渲染和以前一模一样:角标不出现而已。 - 世界可声明带 i18n 标签的角色属性(
characterAttributes):AttributeDefinition.name/description现在接受I18nText(字符串或{ "zh-CN": …, "en-US": … }),world.yaml可自带characterAttributes数组。world-init的 guard 把它原样写成 session 的character-attributesschema,并且优先于跨 session 复用——所以编辑声明会在新 session 生效,而不会继承旧 session 推导/过时的 schema。右栏角色卡按界面语言解析每个标签(resolveI18nText),注入 prompt 的<world-schema>也被深度解析成单一语言(resolveI18nDeep),于是 Haruka 的affection这类自定义字段显示为 好感度 / Affection,而不是落在「其他」里的原始键affection。两个旗舰世界已声明各自的角色字段(mistport:faction / role / tideSense / fogRot / …;haruka:club / affection / trust / …)并带双语标签。未声明的世界行为不变——guard 仍从 dimensions 推导通用属性,再回退到 LLM。见docs/v2/reference/world-data.md与docs/v2/reference/plugins.md。 - 立绘、头像、生成图都可点击放大:把图像生成画廊的预览抽成通用
MediaPreviewDialog。json-renderImage组件新增zoom属性,角色立绘画廊与角色列表头像角标都启用它,图像画廊也改为复用同一个 dialog——放大 + 下载到处一致。头像角标会阻止点击冒泡,避免顺带折叠外层卡片。Image组件还新增framed属性(圆角边框卡片 + hover),让立绘画廊的缩略图和图像生成画廊的卡片视觉一致——看起来像一家人。放大视图是贴合图片的紧凑 lightbox(w-auto),而不是图片浮在大片空白里。 branch-reply「换一个回复」复活为可用功能:这个 GalGame「在 AI 回复间滑动切换」的插件随两个默认世界发布,却是永久不可见的死功能——自举死锁:唯一写候选的入口藏在一个永不渲染的块里的按钮后。现在是真正可用的自动 seed 滑动:每个叙事回合把 narrator 的回复捕获为「原始」(引擎无关——按叙事输出契约发现,不硬编码narrator/chat-mode-narratorid),重新生成控件用 LLM(fast slot、会话语言)产出真实的不同说法供对比,采纳后重写 prompt 历史。原始回复不会被再渲染成卡片,所以一条回复绝不出现两次;旧的英文死板填充已删除。
Changed
- 单一插件配置声明:
userSettings现为玩家可调设置的唯一来源;死的、从未被消费的configmanifest 字段被移除。仍声明config:的旧PLUGIN.md会被 strip + 弃用警告,而非加载失败。PluginUserSettingSpec类型在@covel/shared单一来源(移除两份发散副本),其声明的约束(min/max/options/ type)现已强制——前端 SettingsStore 与服务端resolveUserSettings,越界的世界 / header 值降级为 manifest 默认而非进入 guard 或 hook。 - 插件选择收敛进
pluginPolicy:过期的顶层requiredPlugins/recommendedPlugins/excludedPlugins在加载时折叠(去重)进pluginPolicy,使WorldRecord.metadata的插件选择单一来源。顶层字段在world.yaml仍兼容。每回合的世界读取由短 TTL 的 per-worldId缓存服务。 - 示例世界精选为两个深度打磨的旗舰:内置世界现为 Mistport(雾港·裂潮纪,传统叙事 / 黑暗奇幻悬疑) 与 Haruka Academy(遥风学园,对话 / GalGame 类),各自大幅扩充,并成为新字段的范例。Mistport 新增七名种子角色、第四派系、双语 lore 与
clues/relics/tides记忆块;Haruka 扩到八名角色、五条线,配relationships/promises/rumors/festival记忆块。冗余的cloudmere与neonridge故事世界移入worlds/_archive/(保留参考,不加载)。
Fixed
- 玩家 per-plugin 设置现在真正进入主回合循环:
POST /api/actions(及 resume / plugin-rpc 路径)此前从不解码X-Plugin-User-Settings,主循环里每个 runtime 的userSettings退化为 manifest 默认——玩家在 UI 调的值(chat-mode-narrator 的dialogueRatio、cost-gate 的 token 上限)被静默忽略,仅手动 plugin-rpc 路径生效。主路由现在解码 header 并与世界默认合并。 - 设置 header 不再掩盖世界默认:
X-Plugin-User-Settings此前由全部已注册插件设置构建(store.get()对未设项返回 manifest 默认),所以总是携带一堆默认值,当作"玩家覆盖"盖掉新的世界pluginSettings层。现在只携带玩家显式设过的键(store.has())。 - 被归档的内建世界不再残留在老用户库里:
seedWorlds只 upsert,从包里移除的示例世界(如cloudmere/neonridge)会在每个老用户库里继续 seed 并出现在世界列表。服务器现在在 seed 后跑一次收敛,删除"已不在任何世界源里"的文件 seed 世界。三重安全栏确保绝不误删真实数据:仅触碰metadata.source === "file"的世界(AI 生成的generated/generated-file永不触碰)、仍有存档的陈旧世界保留(打 warn、绝不静默删)、本次没 seed 成任何世界时整体跳过,使瞬时加载故障无法清空世界。见docs/v2/reference/world-data.md。 - 插件 provider-slot 覆盖在开局准备界面生效:function-runtime 插件(如图像生成)通过
modelPresetIduserSetting 指定其 provider slot。开局准备界面此前只读这个设置的 manifest 默认值,所以永远显示默认 slot —— 即使玩家明明配了可用的同类 slot,也会标红「缺少 [covel.<默认>]」,且除了去 Settings → Plugins 改外无法就地指定。现在该行反映玩家的有效覆盖并提供就地下拉选择;选中一个已配置的 slot 即可消除警告,覆盖值通过(本版已修复的)X-Plugin-User-Settingsheader 传到 function runtime。见docs/v2/reference/desktop-config.md。 - 创建带立绘的世界不再在 SQLite 上 500(
database is locked):POST /api/sessions把createSession+ 世界数据导入包在一个withTransaction里。STORE_BACKEND=sqlite时,镜像 media store 对同一个covel.db开了第二个连接,于是在这个写事务进行中物化世界的立绘,就和事务自己的写锁死锁 ——SQLITE_BUSY,5 秒 busy-timeout 超时后报成笼统的 500。现在 SQLite DataStore 与其镜像 media store 按文件共享一个带引用计数的连接,media 写并入打开的事务、随 session 原子提交(设计本意如此),而非与之争锁。无媒体的世界不受影响;只有新的带立绘旗舰世界会触发。 - 导入的角色立绘现在真的会显示:立绘/presence 接线把数据(sha256 寻址的图片 + presence 记录)落了库,但
character-presence右侧面板只把 presence 原始 JSON 倒出来——一堆 sha256 字符串、全程没有一个<img>,玩家根本看不到图。面板现在在顶部渲染立绘画廊(repeat遍历 presence 条目 → 每个角色一个Image,以displayName标注),并作为独立的「角色立绘」tab 排在角色列表正后面——而不是被并进共享的chat-mode(「对话」)组 tab、当成四个 provider 之一藏在 provider 切换后面(那样根本找不到)。没有立绘的世界优雅降级:presence 为空则不显示画廊,缺 avatar 的角色回退到内置「no image」占位块——不裂图、不报错。面板规格在GET /api/ui-specs时重新物化,所以已有 session 下次 reload 就能看到、无需重开。玩家面板不再暴露世界作者的导入表单(那个让你手填图片 sha256 编号 / MIME / 字节大小的东西)。现在是一个自定义PortraitGallery组件:每张立绘是带边框、可点击放大的缩略图,hover 出现「替换」——玩家直接选一张图片,经新增的POST /api/media?sessionId=(会话所有、内容寻址)上传,再通过插件自己的 runtime 写回为该角色的 presence。没有内部字段、没有 JSON。世界作者仍可通过世界包(presence+mediasource)批量导入。 - 桌面端媒体(立绘、生成图)不再加载失败:打包桌面 sidecar 以
NODE_ENV=production运行,此时服务端拒绝用临时的 per-process secret 签发媒体访问 token——但桌面壳只注入了 REST bearer token,从没注入COVEL_MEDIA_TOKEN_SECRET。于是GET /api/sessions/:id/media-token报 500,尽管字节就在磁盘上,所有<Media>(角色立绘、图像生成产物)都显示「media unavailable」占位。桌面壳现在每次启动生成一个媒体 token secret 并注入(用户通过 process env / 仓库.env自设的COVEL_MEDIA_TOKEN_SECRET仍优先)。token 是 5 分钟 TTL、由前端重新请求,所以每次启动轮换无感知。 - 非对话面板不再藏在「对话」tab 下:
character-blueprint(预设角色)、living-world-rules(世界规则)、player-identity(玩家口吻)各自都声明了group: "chat-mode",于是在每个世界它们的面板都被并进一个字面叫「对话」的 tab——在像 Mistport 这样根本不跑对话模式的传统叙事世界里很违和。现在它们各自成为名字清晰的独立 tab(预设角色/世界规则/玩家口吻);真正只属于对话模式的面板(scene-cast/scene-prompts)保留 chat-mode 分组。Mistport 还把character-blueprint从推荐插件里移除——它是对话模式的角色预设工具,在传统叙事里和char-creator重叠。 - 世界维度在进入 narrator 的 prompt 前就按语言解析好:维度的 i18n 叶子(
{ zh, en }的派系名/描述/基调/开场等)此前以原始双语 JSON 注入{{ world.dimensions }}——浪费 token 还可能让模型串语言;而lore早就在加载时按语言解析了。现在buildWorldContextView用新增的共享resolveI18nDeep把维度深度解析到 session 语言,narrator(及world-init/schema-gen)只看到一种语言。附带:tone/openingScenario用 i18n 记录写时现在也能正确提取(旧的typeof === "string"判断会静默跳过它们)。 - 插件 UI/UX 协同审查(多 agent 审计):对全部内建插件「面板 ↔ 上下文 ↔ 工具」接线的系统性审查,修复了一批「玩家看到的与模型看到的不一致、或某个控件根本不起作用」的协同缺陷:
- 角色默认值在写入边界落库:新增
mergeSchemaDefaults,在create-character与 player-init guard 把 schema 的defaultValue合并进存库fields,使面板(渲染时叠加默认)、模型get-character、prompt 上下文三者一致——此前面板显示的hp 100/100在存库记录和 prompt 里都不存在。 scene-cast的activeSpeakerCount旋钮可用了:它此前声明在chat-mode-narrator(另一个插件作用域),滑块永远到不了真正裁剪 cast 的插件(恒为 2),而 narrator 却被告知一个 cast 永远达不到的人数。设置已移到scene-cast;narrator 改以注入的<active-cast>为准。living-world-rules不再静默丢规则:triggered/evolving规则不填关键词时会映射成关键词门控的 lorebook 条目、永不激活,却显示为 enabled。现在只有带关键词的triggered才门控,其余常驻。cost-gate中止可见:主回合的硬预算中止此前发出空的execution.completed且无任何信号;现在带abortReason(protocol + SSE + UI),玩家会看到原因而非静默空回合。chat-mode-narrator不再每回合双重注入 active-cast 与 NPC 关系上下文(正文内联{{ inputs }}+input.inject各一份)。- 玩家面板去术语化:裸 JSON「完整导入」文本框(player-identity / living-world-rules / character-blueprint)和裸
JsonView「已保存…」倒数据,换成结构化卡片(新增通用EntryCardisActive角标标记当前生效的玩家口吻);删除了 codex 的死ui.messagespec 和与总览重复的原始 JSONworld-entriestab。 - 运行时写出的字符串本地化:工具/hook 写出的标签/正文绕过 i18n 门禁、对 en 玩家显示中文——scene-prompts / guide 徽标(改 I18nText)、director 前言 + pregame 欢迎语(改
ctx.locale解析)、chat-mode-narrator正文(补PLUGIN.en.md)。check-plugin-i18n扩展到扫描 handler.js的label/title/placeholder字面量以防回归(它抓到了一个真实的 pregame 泄漏)。 memory.character_relationships重新限定为玩家中心的羁绊,与npc-graph的 NPC↔NPC 图谱互补而非重复(经仔细排查:盲目合并会退化玩家关系的连续性)。
- 角色默认值在写入边界落库:新增
0.0.9 - 2026-06-28
A playability-loop pass — function runtimes become visible in the trace timeline, stale suspensions expire, and player-input narrative localizes by session locale — plus a follow-up engineering batch: multi-node S3 media metadata on Postgres, plugin-utils provider-call tracing, a /debug cost panel, and community plugin uninstall/revoke. The default world and bundled plugins are behavior-unchanged.
Added
- Function-runtime trace coverage (A2-P1-5). Function runtimes were near-invisible in
/debug— nothing betweenruntime.started/completed, and zero rows forctx.gatewayprovider calls. They now emitfunction.executing/function.completed(handler boundary) and, via awithGatewayTracewrapper applied at execution time,gateway.calling/gateway.responded/gateway.failedforgenerateText/generateObject— all persisted totrace_eventsand broadcast, so a function runtime's LLM usage is as visible as an agent runtime's. The five events join the single-sourceCovelEventunion (compile-time exhaustive overCOVEL_EVENT_META+ the frontend switch). A missing handler now emits a terminalruntime.failedinstead of leaving a hangingruntime.started. - Plugin-utils provider-call trace. Closes the A2-P1-5 follow-up: image plugins (and any plugin owning its wire) call providers via
ctx.utils.fetchWithRetry, which awithUtilsTracewrapper now traces asutils.fetch.calling/responded/failed(trace-only,forwardToActionStream:false) at both the function-runtime and agent-guard injection sites. PII-safe — payloads carry only host / method / status / durationMs, never the full URL, query, or API key. - Multi-node S3 media metadata on Postgres.
createPgS3MetadataAdapter(+…FromClient) implements theS3MediaMetadataAdapterinterface over the sharedmedia_assets/media_refsPG tables, so S3-backed media survives restarts and is shared across nodes — the SQLite adapter only covered a single node. Mirrors the SQLite adapter 1:1 and passes the same media-store contract suite against a real Postgres. /debugtoken cost panel. A new Cost view aggregatesusagefromllm.responded/gateway.respondedtrace events by runtime, by turn, and session-total (zero-dependency CSS bars), aggregating generically by event type + runtime id. USD cost is a pending follow-up —llm.respondedpayloads don't yet carry the model id needed forusage × pricing.- Community plugin uninstall + approval revoke.
DELETE /api/plugins/:idremoves a third-party plugin from the user plugins dir (rejects builtin ids, returnsrestartRequired:true);DELETE /api/sessions/:id/approvals[?pluginId=]revokes cached approval grants via the newgate.revoke. The Settings → Packages pane lists installed third-party plugins with an uninstall button. Closes the only missing stage of the community discover→approve→import→active lifecycle (the import stage was already live; docs were stale).
Changed
submit-formis now locale-aware. Theconfirmation{{confirmed}}value (确认/取消) and the fallback-narrative prefixes ([玩家输入]/[玩家选择]/[玩家确认]/[玩家取消]) were hardcoded Chinese; they now resolve by session locale (threaded in via a newRpcHandlerContext.locale, sourced fromsession.localein the plugin-rpc dispatch — no executor change).en-USyieldsConfirm/Cancel+[Player input]/…; unknown locales fall back to zh-CN, byte-for-byte identical to the previous output.submit-form'sSubmission.typenow references the single-sourceInteractionTypeunion.- Dependencies bumped to latest stable. All workspace dependencies updated to their latest stable under the existing
minimumReleaseAge: 10080(1-week) gate — including the major bumps@hono/node-server1→2,electron41→42,@types/node25→26,@json-render/*0.18→0.19, andzod4.3→4.4. One breaking change handled: zod 4.4 treats a barez.unknown()inside a.strict()object as a required key, so the plugin user-settingdefaultfield gained an explicit.optional(). Verified across the full workspace lint + test (incl. real Postgres), server boot, API e2e, and the desktop (electron 42) typecheck. - Dependency-hygiene gate + scaffold alignment. Added
knipas a CIdeps:checkgate (catches unused/missing workspace deps; understands JSDoc type imports + test files, so type-only/test-only deps aren't false-flagged), cleaned 12 stale plugin devDeps, and aligned the@covel/createscaffold to emit correctly-layered minimal deps per template. The authoring guide gains a dependency-layering + extraction-threshold section.
Fixed
- Function runtimes no longer leave a hanging
runtime.startedwhen the handler is missing or throws — a terminalruntime.failed(andfunction.completed{status:failed}) is emitted on every exit path. /api/ui-specsno longer 500s when one plugin's runtime fails to load. A single bad runtime (corrupt UI spec, missing handler dep) used to take down the whole response on every world open; it is now logged and skipped while healthy plugins still resolve.app.onErroradditionally logs request context (method + full URL) for every 500.- Desktop packaging now stages plugin-only workspace deps. 5 bundled function plugins depend on
@covel/plugin-handlers-utils(used by plugins, never by@covel/server), whichpnpm deploy --filter @covel/serverleft out — so every packaged build shipped them broken withERR_MODULE_NOT_FOUND. The desktop build now scans each plugin's@covel/*runtime deps and stages any the server deploy missed; the post-staging smoke test fails on plugin-load errors instead of swallowing them.
中文(备份翻译)
一次可玩性闭环整理(function runtime 在 trace 时间线可见、陈旧挂起项过期、玩家输入叙事按会话 locale 本地化),外加一批工程收尾:Postgres 上的多节点 S3 媒体元数据、plugin-utils provider 调用 trace、/debug 成本面板、社区插件卸载/撤销。默认世界与内置插件行为不变。
Added
- Function-runtime trace 覆盖(A2-P1-5):function runtime 此前在
/debug几乎不可见——runtime.started/completed之间空白,ctx.gatewayprovider 调用零记录。现在发射function.executing/function.completed(handler 边界),并经执行期withGatewayTrace包裹对generateText/generateObject发gateway.calling/gateway.responded/gateway.failed——全部持久化到trace_events并广播,使 function runtime 的 LLM 用量与 agent runtime 同等可见。五个事件纳入单一真相CovelEventunion(对COVEL_EVENT_META与前端 switch 编译期穷尽)。缺失 handler 现在发终结runtime.failed,不再留悬空runtime.started。 - Plugin-utils provider 调用 trace:收尾 A2-P1-5 follow-up。图像插件(及任何自带 wire 的插件)经
ctx.utils.fetchWithRetry调 provider,现由withUtilsTrace包裹器在 function-runtime 与 agent-guard 注入处 trace 为utils.fetch.calling/responded/failed(trace-only,forwardToActionStream:false)。PII 安全——负载仅含 host / method / status / durationMs,绝不含完整 URL、query、api key。 - Postgres 上的多节点 S3 媒体元数据:
createPgS3MetadataAdapter(+…FromClient)在共享media_assets/media_refsPG 表上实现S3MediaMetadataAdapter接口,使 S3 媒体跨重启存活、跨节点共享——此前 SQLite 适配器只覆盖单节点。1:1 镜像 SQLite 版,并对真实 Postgres 通过同一 media-store 契约套件。 /debugtoken 成本面板:新增 Cost 视图,按 runtime / turn / 会话总计聚合llm.responded/gateway.responded的usage(零依赖 CSS 条形),按事件类型 + runtime id 通用聚合。美元成本为待办 follow-up——llm.responded负载尚未携带usage × pricing所需的 model id。- 社区插件卸载 + 审批撤销:
DELETE /api/plugins/:id从用户插件目录删除第三方插件(拒绝内置 id,返回restartRequired:true);DELETE /api/sessions/:id/approvals[?pluginId=]经新增gate.revoke撤销缓存授权。Settings → Packages 面板列出已安装第三方插件并提供卸载按钮。收尾社区 discover→approve→import→active 生命周期唯一缺失的阶段(import 阶段早已实现,文档此前陈旧)。
Changed
submit-form现按 locale 本地化:confirmation的{{confirmed}}取值(确认/取消)与回退叙事前缀([玩家输入]/[玩家选择]/[玩家确认]/[玩家取消])此前写死中文;现按会话 locale 解析(经新增的RpcHandlerContext.locale注入,来源是 plugin-rpc dispatch 的session.locale,无需改 executor)。en-US产出Confirm/Cancel+[Player input]/…;未知 locale 回落 zh-CN,与改前输出逐字一致。submit-form的Submission.type现引用单一真相InteractionTypeunion。- 依赖升级到最新 stable:全部 workspace 依赖在既有
minimumReleaseAge: 10080(1 周)门控下升到最新 stable——含跨 major 的@hono/node-server1→2、electron41→42、@types/node25→26、@json-render/*0.18→0.19、zod4.3→4.4。处理了一处 breaking:zod 4.4 把.strict()object 内裸z.unknown()当作必填 key,故插件 user-setting 的default字段显式加.optional()。已通过全 workspace lint + test(含真实 Postgres)、server 启动、API e2e 与 desktop(electron 42)typecheck 验证。 - 依赖卫生 gate + 脚手架对齐:新增
knip作为 CIdeps:check关卡(拦截 unused/missing workspace 依赖;识别 JSDoc 类型引用与测试文件,不误杀 type-only/test-only 依赖),清理 12 个 stale 插件 devDep,并让@covel/create脚手架按 template 生成正确分层的最小依赖。插件指南新增"依赖分层与复用规范"章节。
Fixed
- function runtime 在 handler 缺失或抛错时不再留悬空
runtime.started——每条退出路径都发终结runtime.failed(及function.completed{status:failed})。 /api/ui-specs不再因单个插件 runtime 加载失败而 500:一个坏 runtime(损坏的 UI spec、缺失的 handler 依赖)此前会在每次打开世界时拖垮整个响应;现在记录并跳过,健康插件照常返回。app.onError还会为每个 500 记录请求上下文(method + 完整 URL)。- 桌面打包现在 stage 插件专属的 workspace 依赖:5 个内置 function 插件依赖
@covel/plugin-handlers-utils(只被插件用、不被@covel/server用),pnpm deploy --filter @covel/server把它漏掉了——导致每个打包版本都带着ERR_MODULE_NOT_FOUND的坏插件。桌面构建现在扫描每个插件的@covel/*运行时依赖并补 stage server deploy 漏掉的;打包后的 smoke test 现在会因插件加载失败而失败,不再静默吞掉。
0.0.8 - 2026-06-28
Eighth public release. Finishes the v0.0.7 architecture pass — clears the remaining audit debt, makes the schema and transaction layers single-source-of-truth, and ships semantic (vector) memory recall, all verified end-to-end against a real pgvector Postgres. New game genres can declare their own memory blocks, and a session can switch storage backends without any behaviour change. The default world and bundled plugins are behavior-unchanged.
Added
- Semantic (vector) memory recall. The memory tier now embeds turn messages (recall) and lorebook + character records (archival) on write — a post-turn, best-effort sweep that never blocks the turn — and serves KNN recall over them, falling back to keyword search per-session when no embedding model is locked. Embed-on-write ingestion is incremental (a persisted cursor + content hashes) and self-heals deleted records; backfill of existing sessions runs in the same path. Wired through a single injected
embedseam so@covel/memorystill depends only onshared+store. Verified end-to-end on real pgvector Postgres. - A real-pgvector
vector-storecontract branch (PgStore) — the production vector path (upsertVector/searchVectors/deleteVectorsvia thevectortype + pgvector operators) now has automated coverage it previously lacked, plus a memory-vector × PgStore integration test. @covel/settingspackage — the unifiedSettingsStore+ its platform backends (browser localStorage, Electron-IPC json-file) split out of@covel/shared, so pure-types consumers no longer pull in browser/Electron code.
Changed
- Store schema is now single-source-of-truth. The boot DDL is derived at module load from the Drizzle schema (
buildCreateTablesSql), so a column or index is declared once and the executed DDL can never drift from it; only the bits Drizzle can't model (triggers, idempotent column migrations) stay hand-written. Identifiers are quoted uniformly. - Transactions are scoped to a single commit. Production turn-commit / session-create / world-data-sync / snapshot-fork callers moved from the global imperative
beginTx/commitTxshim towithTransaction(fn)(real pooled-Postgres transactions; nested calls on serial backends are rejected, not deadlocked), removing the all-database serialization window. runtime/src's 56 flat files are organized into 13 sub-domain directories (trigger / schedule / agent-loop / commit / session / trace / snapshot / rpc / resume / llm / retry / function-runtime); the public barrel is byte-identical.- Cleared remaining v0.0.7 audit debt: priority-band literals collapse to
isPreGamePriority()/NARRATOR_PRIORITY; the two frontend SSE channels share one event reducer;CompactorLLMAdapter+MemoryLLMAdapterconverge into a sharedSimpleCompletionAdapter; the Anthropic cache-breakpoint cap is one shared constant. - Removed the duplicate turn entrypoint.
POST /api/sessions/:id/turnwas a mounted-but-frontend-unreachable second turn pipeline;/api/actionsis now the single turn-execution route. Its tests migrated to/api/actions. - Bumped all monorepo package versions
0.0.7→0.0.8.
Fixed
- Cross-backend vector parity. A real-PG vector contract immediately surfaced two PgStore-only bugs the Memory/SQLite backends hid: a
freshSchemastore kept stale rows because the dynamicvec_mem_*tables were not dropped, and the upsert/delete paths were untested. A fresh store now starts empty on every backend — the same data, the same API, switch backend freely. - memory recall data-loss / staleness. The recall cursor no longer advances past a message that got an empty embedding (which would drop it from recall forever); short vector results during backfill are topped up with keyword hits so the most recent messages aren't missed; deleted lorebook/character vectors are purged instead of returned as stale hits.
- Snapshot-fork orphaned media refs.
mediaStore.addRef(a cross-store write the DataStore transaction can't roll back) moved to run after the fork commits, so a rolled-back fork (e.g. the cursor-missing 409 path) can no longer leave an orphan ref. - Postgres contract suite no longer races the system catalog under parallel runs (per-file database isolation + single-connection
freshSchemaDDL).
中文(备份翻译)
第八个公开版本。收尾 v0.0.7 的架构整理——清掉剩余审计债,让 schema 与事务层成为单一真相源,并交付向量语义记忆召回,全部用真实 pgvector Postgres 端到端验证。新游戏类型可声明自己的记忆块;一个会话可在不改变任何行为的前提下切换存储后端。默认世界与内置插件行为不变。
Added
- 向量语义记忆召回:记忆层在写入时把 turn 消息(recall)与 lorebook + 角色记录(archival)embedding(回合后 best-effort sweep,绝不阻塞回合),并对其做 KNN 召回;未锁定 embedding 模型时按会话回退关键词检索。写入时 ingestion 增量(持久游标 + 内容哈希)、自愈已删记录、历史会话回填走同一路径。经单一注入的
embedseam 接入,@covel/memory仍只依赖shared+store。真实 pgvector Postgres 端到端验证。 vector-store契约新增 PgStore(真 pgvector)分支——生产向量路径此前零自动化覆盖,现已补上,外加 memory 向量 × PgStore 集成测试。@covel/settings包——统一SettingsStore及平台后端(浏览器 localStorage、Electron-IPC json-file)从@covel/shared拆出,纯类型消费方不再被迫拖入浏览器/Electron 代码。
Changed
- 存储 schema 单一真相源:boot DDL 在模块加载时由 Drizzle schema 派生(
buildCreateTablesSql),列/索引只声明一次、执行的 DDL 不可能漂移;只有 Drizzle 无法建模的部分(触发器、幂等列迁移)保留手写。标识符统一加引号。 - 事务作用域绑定到单次提交:生产的回合提交/建会话/world-data 同步/快照 fork 调用方从全局命令式
beginTx/commitTx垫片迁到withTransaction(fn)(真实 Postgres 池化事务;串行后端的嵌套被拒绝而非死锁),消除全库串行化窗口。 runtime/src的 56 个扁平文件整理进 13 个子领域目录;公开 barrel 逐字节不变。- 清掉剩余 v0.0.7 审计债:priority band 字面量收成
isPreGamePriority()/NARRATOR_PRIORITY;前端两条 SSE 通道共用一个事件 reducer;CompactorLLMAdapter+MemoryLLMAdapter合并为共享SimpleCompletionAdapter;Anthropic 缓存断点上限收成单一常量。 - 移除重复的回合入口:
POST /api/sessions/:id/turn是已挂载但前端不可达的第二条回合管线;/api/actions现为唯一回合执行路由,其测试已迁移。 - 所有 monorepo 包版本
0.0.7→0.0.8。
Fixed
- 跨后端向量一致性:真 PG 向量契约立刻暴露两个 Memory/SQLite 后端掩盖的 PgStore 专属 bug——
freshSchema的 store 因动态vec_mem_*表未被 drop 而残留旧数据,且 upsert/delete 路径未测。现在 fresh store 在每个后端都从空开始——同样的数据、同样的 API、自由切换后端。 - memory 召回数据丢失/陈旧:召回游标不再越过 embedding 为空的消息(否则永久丢失);回填期向量结果不足时用关键词补足,不漏最近消息;已删 lorebook/角色向量被清除而非作为陈旧命中返回。
- 快照 fork 孤儿 media ref:
mediaStore.addRef(DataStore 事务回滚不了的跨 store 写)挪到 fork 提交后执行,回滚的 fork(如 cursor-missing 409)不再留下孤儿 ref。 - Postgres 契约套件在并行下不再竞争系统目录(每文件独立库 + 单连接
freshSchemaDDL)。
0.0.7 - 2026-06-27
Seventh public release. An architecture-optimization pass over the kernel and store: duplicated contracts collapse to single, compile-time-enforced sources of truth; the SQLite and Postgres store query layers unify behind one shared adapter with cross-backend parity verified against a real Postgres; and a batch of latent correctness bugs are fixed. The default world and bundled plugins are behavior-unchanged.
Added
memoryBlocksmanifest field +MemoryBlockSchema: core memory blocks are now declared as data by a plugin or world package (the default four — story state / relationships / scene / player profile — ship on the builtinmemoryplugin and are aggregated at bootstrap by trust tier). A new game genre can define its own blocks (clues/suspects/ …) without forking the framework core- cost-gate per-session token budget via
userSettings, read in-hook throughHookContext.getOwnSettings(), with aCOST_GATE_SOFT/HARD_TOKENSenv fallback for env-only deployments /api/ui-specsnow validates each panel spec against a Zod schema with aspecVersion, returns per-spec diagnostics instead of a generic error, and caches discovery by content signature instead of re-scanning + rewriting on every request- A real-Postgres-verified store contract run (713 cases) plus new parity / drift-guard tests: proposal-type ↔ commit-handler ↔ discovery alignment, the SSE event union, hook events, DDL ↔ Drizzle index consistency, table-registry coverage, cross-backend null /
compactedAtTurnIdround-trip, and the prompt cache-breakpoint limit
Changed
- Single sources of truth (compile-time exhaustive). Proposal types (payload map + discriminated union, handlers
satisfies Record<ProposalType, …>), SSE events (oneCovelEventunion driving the forward whitelist + an exhaustive frontend switch), hook events (HOOK_EVENTS), framework capabilities (typed registry), and provider protocols (ProtocolRegistry) each now live in one place — adding one is a single edit and a missing handler/case is a compile error - Store query layer unified. The mirrored SQLite/Postgres record modules collapse behind one async
SqlRunner(better-sqlite3's sync driver wrapped awaitable, postgres-js already async); SQLite snapshots / suspensions converted from hand-written SQL to drizzle, so the SQLite backend now has zero hand-written record SQL. Cascade-delete / drop-list / memory-snapshot table sets derive from a single table-registry, andwithTransaction(fn)scopes a transaction to one commit (real pooled Postgres transaction; nesting on serial backends is rejected, not deadlocked) DataStore's 82-method god-interface split into 21 domain sub-interfaces composed back intoDataStore(shape unchanged);gateway.ts's 7 operations collapse into onerunOperation;parsePluginMd's repeated lenient-field blocks become a data-driven tableLLMAdapter/LLMResponsemoved to@covel/shared, removing the only wrong-layer edge in the dependency graph (create → runtime)- Trigger modes
conditional/error-retryare explicitly marked reserved (they never fire in production); in-turn event fan-out reuses the singleshouldTriggerauthority - Deleted the never-implemented
SessionTransportinterface and unused command-protocol types; pruned unwired prompt-assembler stubs; hoisted the Anthropic cache-breakpoint cap to a single sharedMAX_CACHE_BREAKPOINTS - memory recall / archival are now honestly documented as keyword-based — the vector primitives exist but the embed-on-write ingestion path does not, so a vector searcher would query empty tables; the swap seam is reserved and documented
- Bumped all monorepo package versions
0.0.6→0.0.7; refreshed the README bundled-plugin table (16 → 19, addingcost-gate/director/story-guard)
Fixed
- Resume lost runtime resilience. A suspended-then-resumed agent runtime bypassed retry / loop-detection / streaming (it called the LLM directly); resume now folds into the shared tool loop and goes through the same retry policy as a normal turn
- Cross-backend data loss. SQLite
appendTurnMessagedroppedcompactedAtTurnIdon insert while Postgres / memory / IDB persisted it; all backends now agree (pinned by a parity contract test).createSession/updateSessionalso serialize optional fields uniformly across backends - Production Postgres now creates the
trace_eventstrace_id/turn_idindexes (declared in Drizzle but absent from the runtime DDL); a DDL ↔ Drizzle consistency test guards the drift openai-responsesstreaming now accumulates function-call deltas — agent runtimes on that protocol previously lost every tool call silently/api/discoveryno longer advertises proposal types with no commit handler (record.upsert/narrative.template, removed); theworking_memory.changedSSE event is in the shared union and is no longer dropped by the frontend- The reserved builtin plugin-id list is derived from the bundled plugins instead of a hand-kept list that had drifted to 8/19 (it guards third-party name-squatting)
- cost-gate's env-var budget fallback is reachable again — a declared
userSettingsdefault silently shadowed it, so a custom env value was ignored - The Postgres contract suite no longer races the system catalog under parallel runs (per-file database isolation + single-connection
freshSchemaDDL)
中文(备份翻译)
第七个公开版本。对内核与存储层的一次架构优化:把重复的契约收口为单一、编译期强制的真相源;将 SQLite 与 Postgres 的存储查询层统一到一个共享适配器之后,并用真实 Postgres 验证跨后端行为一致;并修复一批潜在的正确性 bug。默认世界与内置插件行为不变。
Added
memoryBlocksmanifest 字段 +MemoryBlockSchema:核心记忆块现由插件/世界包以数据形式声明(默认四块——剧情状态/关系/场景/玩家档案——随内置memory插件提供,启动时按信任层级聚合)。新游戏类型可定义自己的记忆块(clues/suspects…)而无需 fork 框架- cost-gate 每会话 token 预算改用
userSettings,hook 内经HookContext.getOwnSettings()读取,并保留COST_GATE_SOFT/HARD_TOKENS环境变量兜底 /api/ui-specs现按 Zod schema(含specVersion)校验每个面板 spec、返回逐 spec 诊断、并按内容签名缓存发现结果(不再每次请求扫盘重写)- 一次真实 Postgres 验证的 store 契约(713 用例),以及新增的 parity / 防漂移测试:proposal 类型↔commit handler↔discovery 对齐、SSE 事件 union、hook 事件、DDL↔Drizzle 索引一致性、表注册表覆盖、跨后端 null /
compactedAtTurnId往返、prompt 缓存断点上限
Changed
- 单一真相源(编译期穷尽):Proposal 类型、SSE 事件(单一
CovelEventunion)、hook 事件(HOOK_EVENTS)、框架 capability(typed registry)、provider 协议(ProtocolRegistry)各自收口到一处——新增一个只改一处,漏一个 handler/case 即编译失败 - 存储查询层统一:镜像的 SQLite/Postgres record 模块收敛到一个异步
SqlRunner之后;SQLite snapshots/suspensions 从裸 SQL 转为 drizzle,SQLite 后端现已零手写 record SQL。级联删除/落表清单/记忆快照的表集由单一表注册表派生;withTransaction(fn)将事务作用域绑定到单次提交(真实 Postgres 池化事务;串行后端的嵌套会被拒绝而非死锁) DataStore的 82 方法上帝接口拆成 21 个领域子接口再组合(形状不变);gateway.ts的 7 个操作收敛为一个runOperation;parsePluginMd的重复 lenient 块改为数据驱动的表LLMAdapter/LLMResponse移到@covel/shared,消除依赖图里唯一的错位边(create → runtime)- 触发模式
conditional/error-retry明确标注为 reserved(生产从不触发);回合内事件 fan-out 复用单一shouldTrigger - 删除从未实现的
SessionTransport接口与未用的命令协议类型;清理未接线的 prompt-assembler 残桩;把 Anthropic 缓存断点上限提成单一共享的MAX_CACHE_BREAKPOINTS - memory recall/archival 现诚实记录为关键词检索——向量原语存在但缺少写入时 embedding 的 ingestion,向量检索会查空表;扩展点已预留并文档化
- 所有 monorepo 包版本
0.0.6→0.0.7;刷新 README 内置插件表(16 → 19,补cost-gate/director/story-guard)
Fixed
- Resume 丢失运行时韧性:挂起后恢复的 agent runtime 绕过了重试/循环检测/流式(它直接调 LLM);resume 现在并入共享工具循环,走与正常回合相同的重试策略
- 跨后端数据丢失:SQLite
appendTurnMessage在 insert 时丢掉compactedAtTurnId,而 Postgres/memory/IDB 保留;现所有后端一致(由 parity 契约测试钉住)。createSession/updateSession的可选字段序列化也跨后端统一 - 生产 Postgres 现会创建
trace_events的trace_id/turn_id索引(Drizzle 声明了但运行时 DDL 缺失);DDL↔Drizzle 一致性测试守护漂移 openai-responses流式现累积 function-call 增量——此前该协议下的 agent runtime 会静默丢掉每一次工具调用/api/discovery不再广告没有 commit handler 的 proposal 类型(已删除record.upsert/narrative.template);working_memory.changedSSE 事件已纳入共享 union,前端不再丢弃- 保留的内置 plugin-id 名单改为从内置插件派生,取代漂移到 8/19 的手维护列表(它用于防第三方占名)
- cost-gate 的环境变量预算兜底重新可达——一个声明的
userSettings默认值曾静默压过它,导致自定义 env 值被忽略 - Postgres 契约套件在并行运行下不再竞争系统目录(每文件独立库隔离 + 单连接
freshSchemaDDL)
0.0.6 - 2026-06-26
Sixth public release. Expands the runtime hook system from 8 to 16 lifecycle events, ships the first plugins that consume them, and lands a batch of runtime-architecture refactors and static-audit fixes. The default world and bundled plugins are behavior-unchanged — the new hooks are dormant infrastructure and the three new plugins are opt-in.
Added
- Hook lifecycle expanded 8 → 16 events:
PreLLMCall/PostLLMResponse(non-destructively rewrite a per-call request / patch the response before tool dispatch),PostContextAssembly(turn-level system-prompt / history shaping),PreSchedule(narrow which runtimes run this turn),PreCompaction/PostCompaction(veto / observe history compaction),SessionStart/SessionEnd(session lifecycle), andPostToolUse.terminate(end the tool loop after recording a result) - Session-scoped hook pipeline: the global pipeline now filters hooks by the session's active plugins via
AsyncLocalStorage, so a plugin's hooks only fire for sessions where it is active HookContext.getOwnSettings(): a hook can read its own plugin's per-sessionuserSettings(turn-level, read-only, deep-frozen snapshot)- Three new opt-in plugins — the first hook consumers:
cost-gate(per-session token budget; included by default in the front-end Low Cost pack),director(PostContextAssembly— one consistent narration preamble across all story runtimes), andstory-guard(PostLLMResponsecontent sanitisation +PreToolUsehigh-risk-tool deny-list) EventBus.flush()durability barrier for the best-effort audit-event streamPostContextAssemblypayload carries a read-onlyoutputKindso handlers can target specific runtime kinds without hardcoding plugin ids
Changed
- Bumped all monorepo package versions
0.0.5→0.0.6 TurnStart/PostRuntime/PostToolUsehooks are nowsequential, so their abort / replace paths actually take effect (previously dead code underparallel)- Runtime refactors (behavior-preserving):
AgentLoopDepsnarrow seam isolating the core agent loop from orchestration deps;RuntimeInvocationoptions object replacingexecuteOneRuntime's 19 positional args; a singleresolveTurnCapabilityPluginIdssource for capability-discovered plugin ids - Registered all new hook events in the three plugin-loader whitelists (a plugin declaring them was previously dropped at parse time)
Fixed
- Session-scoped every server commit / hook entry point (turn, actions, plugin-rpc, the commit processor, resume, characters, and session routes) so a plugin's hooks never fire for sessions where it is inactive
PreSchedulecan no longer drop Pre-Game runtimes while Pre-Game is pending — a hook can shape main-loop scheduling but not break session initialization- Resume path now fires
PreLLMCall/PostLLMResponseand scopes the resumed plugin's own hooks (both were silently skipped for a suspended-then-resumed runtime) SessionStart/SessionEndhardened with local try/catch so a handler failure can never turn a committed create / end / delete into a 500- Plugin loader: an object i18n
descriptionis preserved when a plugin also declareshooks(the combination previously failed frontmatter validation) - Added unit coverage for
computeSessionTurnCount(the turn-count module had none) and clarified that an empty main-loop turn counts as a player turn
中文(备份翻译)
第六个公开版本。将运行时 hook 生命周期从 8 个扩展到 16 个事件,交付首批消费这些 hook 的插件,并落地一批运行时架构重构与静态审计修复。默认世界与内置插件行为不变——新 hook 是休眠基础设施,三个新插件均为可选启用。
Added
- hook 生命周期 8 → 16:
PreLLMCall/PostLLMResponse(按调用非破坏性改写请求 / 在工具分发前改写响应)、PostContextAssembly(回合级系统提示/历史塑形)、PreSchedule(收窄本回合运行的 runtime 集)、PreCompaction/PostCompaction(否决/观察历史压缩)、SessionStart/SessionEnd(会话生命周期)、PostToolUse.terminate(记录结果后结束工具循环) - 会话作用域 hook 管线:全局管线经
AsyncLocalStorage按会话激活插件过滤,插件 hook 只对其激活的会话触发 HookContext.getOwnSettings():hook 可读本插件本会话的只读userSettings(回合级、只读、深冻结快照)- 三个新的可选插件(首批 hook 消费者):
cost-gate(每会话 token 预算,默认进前端 Low Cost 组合包)、director(PostContextAssembly给全部 story runtime 注入统一导演前言)、story-guard(PostLLMResponse内容净化 +PreToolUse高危工具拦截) EventBus.flush()持久化屏障;PostContextAssemblypayload 增加只读outputKind
Changed
- 所有 monorepo 包版本
0.0.5→0.0.6 TurnStart/PostRuntime/PostToolUse改为sequential,其 abort/replace 分支才真正生效(此前在parallel下是死代码)- 运行时重构(行为保持):
AgentLoopDeps窄接缝隔离核心 agent 循环、RuntimeInvocation选项对象替代 19 个位置参数、resolveTurnCapabilityPluginIds单一来源 - 三个 loader 白名单注册全部新 hook 事件
Fixed
- 会话作用域覆盖所有 server commit/hook 入口(turn/actions/plugin-rpc/提交处理器/resume/characters/session 路由)
PreSchedule在 Pre-Game pending 时不能丢弃 Pre-Game runtime- resume 路径接入
PreLLMCall/PostLLMResponse并修正 resume 时被恢复插件自身 hook 的作用域 SessionStart/SessionEnd本地 try/catch 固化 observe-only- 插件加载器:i18n 对象
description与hooks并存不再校验失败 - 补
computeSessionTurnCount单测,并澄清空主循环回合计为玩家回合
0.0.5 - 2026-06-16
Fifth public release. An internal, code-quality-focused refactor: systematic de-duplication across storage backends and the plugin layer, decomposition of oversized files, unified conventions, and isolation/data-flow fixes. No user-facing behavior change (except an intentionally unified API error-response envelope).
Added
- New
@covel/plugin-handlers-utilspackage providing shared pure-function helpers for plugin function-runtime handlers (eliminates verbatim-duplicated helpers and proposal construction across 5 plugins) - New
FrameworkCapabilityconstant and type inpackages/shared, consolidating framework-consumed capability tags so bare-string typos can no longer silently disable features - Unified API error-response envelope
ApiErrorResponsewith anerrorBodyfactory
Changed
- Bumped all monorepo package versions
0.0.4→0.0.5 - Store: extracted shared cross-backend mappers/insert-values, removing PG/SQLite duplication (~1145 lines); split
types.tsandcommon/mappersby domain - Runtime/Context: extracted commit validators and LLM telemetry; split turn-agent-tool-loop / llm-retry / turn-agent-runtime / prompt-assembler
- AI-Provider: de-duplicated adapter parameter extraction / metadata sanitizing; externalized model-capability data from inline TS (950 lines) to JSON; de-duplicated gateway fallback; split env registry and plugin schema by domain
- Server: unified error envelope, replaced 37+ session-404 checks with a
resolveSessionParammiddleware, split the bootstrap/install/worlds mega-routes - Web: removed dead code, decomposed several oversized components by responsibility, unified silent error-swallowing into a visible
ignoreError
Fixed
- Fixed
characters.tshardcoding a framework plugin ID in violation of the framework↔plugin isolation rule (now usesframeworkProposalSource) - Fixed a PG
value-field NULL-semantics regression introduced by cross-backend de-duplication (restored returningnull, unified across both backends) - Fixed character-panel staleness on the char-creator write path (restored and improved the post-turn snapshot resync)
- Fixed a React anti-pattern where the confirmation dialog ran side effects inside a setState updater
中文(备份翻译)
第五个公开版本。一次以代码质量为核心的内部重构:消除跨后端与插件层重复、拆分巨型文件、统一约定、修复隔离与数据流问题。对外行为保持不变(除有意统一的 API 错误响应信封)。
Added
- 新增
@covel/plugin-handlers-utils包,为插件 function-runtime handler 提供共享纯函数工具(消除 5 个插件中逐字重复的 helper 与 proposal 构造) packages/shared新增FrameworkCapability常量与类型,收敛框架消费的 capability 标签,避免裸字符串拼写漂移导致功能静默关闭- 统一 API 错误响应信封
ApiErrorResponse与errorBody工厂
Changed
- monorepo 全量版本号
0.0.4→0.0.5 - Store:抽取跨后端共享 mapper/insert-values,消除 PG/SQLite 重复(约 1145 行);
types.ts与common/mappers按域拆分 - Runtime/Context:抽取 commit 验证器与 LLM 遥测,拆分 turn-agent-tool-loop / llm-retry / turn-agent-runtime / prompt-assembler 等大文件
- AI-Provider:适配器参数提取/元数据清理去重;模型能力数据由内联 TS(950 行)外置为 JSON;gateway fallback 去重;env registry 与 plugin schema 按域拆分
- Server:错误信封统一、
resolveSessionParam中间件替换 37+ 处会话 404 检查、bootstrap/install/worlds 大路由拆分 - Web:清理死代码、按职责拆分多个巨型组件、统一静默吞错为可见的
ignoreError
Fixed
- 修复
characters.ts硬编码框架插件 ID 违反框架↔插件隔离规则(改用frameworkProposalSource) - 修复跨后端去重引入的 PG
value字段 NULL 语义回归(恢复返回null,两后端统一) - 修复角色面板在 char-creator 写入路径下的同步问题(恢复并改进 turn 完成后的快照重同步)
- 修复确认对话框在 setState updater 内执行副作用的 React 反模式
0.0.4 - 2026-05-28
第四个公开版本。重点收敛回合流稳定性、插件/会话解析、框架可见文本本地化、插件模板质量与发布文档。
Added
- 新增静态回合审计 skill,用于检查 turn flow、插件边界与运行时输出相关风险
- 插件模板新增 runtime cases 与可运行 note/analyst 示例,create-plugin / create-world skills 补齐验证指引
- 桌面主进程错误与启动文案补齐中英文 i18n 支持
Changed
- monorepo 全量版本号
0.0.3→0.0.4 - 加固 turn flow、插件解析、会话插件 API、snapshot / trace / working-memory 等运行时边界
- 简化 prompt context feature gates,整理 context builder、prompt assembler 与 serialization 相关实现
- 刷新 production Docker image、release docs、README 与贡献文档;移除过期本地开发草稿和废弃模板脚手架
Fixed
- 修复框架 UI 中残留的硬编码可见文本,补齐对应中英文 locale
- 修复插件 metadata、runtime loading、form submission、suspend/resume 与 post-turn memory 相关测试覆盖
- 修复 desktop asset import、IPC handler、splash/startup error 路径与 release staging 相关边界
0.0.3 - 2026-05-11
第三个公开版本。重点收敛世界数据导入、生成世界持久化、插件目录元数据、存储后端边界、桌面发布链路与长期维护性重构。
Added
- 世界数据导入管线:支持世界包声明式
worldData数据源、会话创建时导入、同步 API、导入 ledger、角色蓝图与媒体引用同步 - AI 生成世界新增保存目标:
server-file/server-store/return-only,前端依据/api/health.storage.data.frontendMode选择合适持久化路径 - 插件目录元数据新增 tags、relations 与世界级
pluginPolicy,会话准备页支持按世界策略推荐、筛选与选择插件 - 生成世界质量门、世界数据 schema 校验、插件 README 检查与 Playwright e2e 稳定性验证脚本
Changed
- monorepo 全量版本号
0.0.2→0.0.3 - 桌面发布链路收敛到 Electron,移除已废弃的 Tauri shell,更新 release workflow 与 staging smoke 验证
- 存储架构统一为 DataStore / MediaStore / VectorStore 边界,浏览器本地模式使用 IndexedDB,远端模式继续走服务端持久化
- 重构长期维护文件:拆分 bootstrap、plugin RPC、turn pipeline、store 后端、desktop IPC/logging、web session prep/debug route 等大模块
- README、首页 demo 与视觉层级更新,刷新 demo GIF 资源与玩家视角说明
Fixed
- 加固插件执行安全边界、world data schema/media sync、生成世界包输出、gateway slot fallback 与 provider 参数覆盖测试
- 修复重复动态/静态导入、桌面 sidecar/staging 构建路径、plugin RPC/SSE 边界、存储后端空值与媒体生命周期相关边界
Documentation
- 重组开发文档,补齐 storage architecture、world data、plugin authoring、desktop state、refactor follow-up 与插件 README
0.0.2 - 2026-04-29
第二个公开版本。围绕 2026-04-29 代码库审计发现的 7 个问题做收敛——CI 红灯、桌面安全、插件生态闭环、首屏体积。
Added
- 桌面 sidecar 启动时生成一次性 bearer token(
COVEL_DESKTOP_REST_TOKEN),所有PUT /api/config/{keys,settings,data-root}与POST /api/config/open-folder必须带Authorization: Bearer <token>。读接口保持开放;token 未注入时(pure web / dev / Demo Host)自动 no-op,行为兼容 /api/config/info新增requiresAuth字段,前端据此决定是否附加 Authorization 头- 社区插件
tools.local激活生命周期:activatePluginLocalTools(pluginId)在 RPC 审批通过后 just-in-time 注册到toolMap,并在 approvals decision=allow 后预激活;幂等 - Electron 外链 allowlist:
https:直接放行 + 写审计日志;非 loopbackhttp:弹用户确认 dialog;其他协议(javascript:/file:/自定义)拦截 - 桌面 sidecar awaitable shutdown:新
stopServer()等待子进程exit事件后再启动新 sidecar,5s 超时 SIGKILL,重启路径告别端口/SQLite 锁竞态
Changed
- monorepo 全量版本号
0.0.1→0.0.2 - web 首屏 bundle 拆分:vite manualChunks 抽出 react/router/i18n/markdown/graph/motion 6 个 vendor chunk;主 chunk 490 kB → 365 kB gzip(-25%)
- README + web 首页 demo 资源换为最新 dev3 视频(3× 速、无音轨、960×568 GIF + 1280×756 MP4)
PluginRpcResponse.failedJobs字段标记 deprecated;expectsBackgroundFollower路径统一返回 202accepted+ jobId,失败状态落在_jobs/<jobId>的reason: "expected-background-follower-missing"- i18n 扫描器白名单覆盖 settings/theme 的 bilingual config 目录(
{ "zh-CN", "en-US" }自带翻译的对象不再误报);database-panel raw 字符串迁移到 locale;删除冗余t(key, "中文")默认值 - CORS 收窄:从「任意 loopback origin」改为「dev origin(5173)+ sidecar own origin(serverPort)+
CORS_ORIGIN显式配置」
Fixed
pnpm test之前因tests/api/plugin-rpc.test.ts期望 200 实得 202 红灯——契约已确定为 202 异步 job 模式,测试同步更新到轮询_jobs失败状态- 重复静/动态 import 警告:
reload-overlay、settings/store不再同时被静态和动态引入,Vite 不再警告 ineffective dynamic import pnpm check:i18n35 处 raw CJK literal 全部清理,回到绿灯
Documentation
docs/v2/reference/api.md:202 示例补phase字段;_jobsschema 补reason字段docs/v2/reference/desktop-config.md:新增「桌面 REST 写接口的 token 门」章节docs/v2/plugins/advanced.md:澄清社区插件tools.local在审批通过后的延迟激活语义
0.0.1 - 2026-04-25
首个公开稳定版本。在 0.0.1-beta 基础上做了一轮可发布性收敛。
Added
- 框架定位为 agentic role-playing game framework(README 中英文重写以体现这一定位)
- GitHub Actions release workflow 支持
git push v*tag 自动 build 并发布 GitHub Release - Electron 打包产出收敛到「DMG + ZIP」两个文件,新增
apps/desktop/scripts/cleanup-artifacts.mjs在afterAllArtifactBuild钩子里清理 blockmap、latest-*.yml等 auto-update 元数据和解包目录 - create-plugin / create-world skill 增加测试与验证指引(
references/plugin-testing.md、references/world-validation.md),含 vitest + MockLLM + harness 模板和 schema/引用一致性/lore 覆盖度脚本
Changed
- monorepo 全量版本号
0.0.1-beta→0.0.1(root + 4 apps + 12 packages + 7 plugins + 2 templates + Tauri 3 处) electron-builder.yml增加publish: null抑制 update manifestdocs/v2/plugins/essentials.md附录 B 插件清单按plugins/**/PLUGIN.md真实 frontmatter 重列(priority、runtime 类型与仓库实际一致)docs/v2/reference/plugins.md多 runtime 目录示例改为中性占位,避免与真实world-init/schema-gen单 runtime 现状混淆- README/web 首页 demo 资源刷新为最新 dev 视频(3× 速、去音轨、800px GIF + 1280p MP4)
Fixed
.claude/skills/create-plugin/references/example-plugins.md修正过时的model: ds(实际 slot 为story/plugin) 和与真实插件不符的 priority 数字
0.0.1-beta - 2026-04-19
首个公开 beta 版本。
Added
- 插件驱动的回合执行管线(Trigger Router → Priority Scheduler → Context Assembly → Runtime Runner → Commit Chain)
- 多 Provider LLM 抽象:DeepSeek / Qwen (DashScope) / OpenAI / Anthropic
- 2597 模型能力数据库(LiteLLM 同步),自动识别多模态 / function-calling / reasoning
- 存储后端:Memory / SQLite / IndexedDB / PostgreSQL(Drizzle ORM)
- 核心插件集:
pregame、world-init、char-creator、narrator、guide、npc-graph、codex - 声明式 UI:
json-render+ plugin-ownedui/*.json,无硬编码 Tab - Electron 桌面版:macOS (arm64/x64)、Windows (x64/arm64 NSIS + portable)
- 外部化 prompt 模板与世界包(markdown + yaml)
- i18n:中英双语前端 + plugin 本地化 runtime
- 智能重试:超时 / 首 token 过慢 / 工具调用死循环 自动检测并回退
- 开发期 LLM replay cache(
COVEL_LLM_REPLAY=auto)
Documentation
- 项目 README、LICENSE (MIT)、CONTRIBUTING、CHANGELOG
- 三层文档:
reference/(API/协议)、guide/(作者指南)、architecture/(系统设计) - Release pipeline:
.github/workflows/release.yml