• v4.1.1 5b1bd2a427

    v4.1.1
    All checks were successful
    CI / Backend lint (ruff) (push) Successful in 27s
    CI / Bot/backend version sync (push) Successful in 32s
    CI / Summarisation accuracy eval harness (stub provider) (push) Successful in 1m13s
    Release / Create Forgejo release (push) Successful in 13s
    CI / Frontend tests, audit, and build (push) Successful in 1m42s
    CI / Bot tests and audit (push) Successful in 1m50s
    CI / Docker image build (push) Successful in 4m41s
    CI / Backend migration, tests, and audit (push) Successful in 5m56s
    Release / Build and push versioned images (push) Successful in 7m12s
    Stable

    claude-bot released this 2026-09-05 00:19:52 +00:00 | 2641 commits to main since this release

    A patch on the way to v4.2.0: the AI provider is named rather than guessed
    from its URL, and a /ask answer that ran out of room now says so.

    No migrations. Nothing to rehearse, nothing to back out.

    The bot contract is unchanged (still v1). The one /api/bot/* change is
    an optional field added to the /ask response, which the bot tolerates when
    absent, so backend and bot images can be upgraded independently in either
    order.

    One new optional setting — the AI provider select in Admin → Bot Settings.
    Nothing changes on upgrade unless you set it; see the entry below for who
    should.

    Added

    • [webapp] Admin → Bot Settings can now name the AI provider explicitly
      instead of letting it be detected from the endpoint URL (#351). Detection
      matches on the URL — anthropic.com, openai.com, :11434 — and falls back
      to llama.cpp for everything else, so if you run a gateway or reverse proxy
      on your own domain, your endpoint has been treated as llama.cpp regardless of
      what is behind it.
      The provider decides the JSON-schema format, how
      truncation is detected, and the default context window, so a wrong guess
      degrades summaries quietly rather than failing. Leave it on "Detect from the
      URL" if that guess is already right for you — nothing changes on upgrade.

    Fixed

    • /ask no longer presents a cut-off answer as a complete one (#426). The
      question-answering path never checked whether the model ran out of room, and
      a truncated answer is prose that simply stops — there was nothing to notice.
      It is now marked in the Discord embed. Two related fixes on the same path:
      the output limit was 1024 tokens on Anthropic and OpenAI (small for an answer
      drawn from ten sessions plus the wiki) and unset on llama.cpp, and is now
      4096 everywhere; and on Ollama the question was being silently cut to
      that provider's 2–4k default, so the model answered from a fraction of the
      context it was supposed to have. If your /ask answers have felt oddly
      narrow on a self-hosted Ollama, that is why.
    • [webapp] A silently truncated prompt is now reported on every LLM provider,
      not just Ollama
      (#351). The check that warns when a provider consumed far
      fewer prompt tokens than were sent existed, but was wired to a single code
      path. llama.cpp, Anthropic and OpenAI deployments got no warning at all, so a
      transcript that overflowed the context window could be summarised from its
      tail with nothing in the logs to say so. If you self-host llama.cpp and see
      this warning appear after upgrading, your configured context window is too
      large — see the note under "Context window (tokens)" in Bot Settings.
      The check stays quiet on Anthropic and OpenAI, where prefix caching makes a
      low count ambiguous rather than alarming.
    Downloads
  • v4.1.0 68ec94ffef

    v4.1.0
    All checks were successful
    CI / Backend lint (ruff) (push) Successful in 46s
    CI / Docker image build (push) Successful in 9s
    Release / Create Forgejo release (push) Successful in 12s
    CI / Bot/backend version sync (push) Successful in 41s
    CI / Summarisation accuracy eval harness (stub provider) (push) Successful in 1m42s
    Release / Build and push versioned images (push) Successful in 54s
    CI / Frontend tests, audit, and build (push) Successful in 2m15s
    CI / Bot tests and audit (push) Successful in 2m32s
    CI / Backend migration, tests, and audit (push) Successful in 7m37s
    Stable

    claude-bot released this 2026-09-01 18:11:30 +00:00 | 2653 commits to main since this release

    Data Durability & Recovery. Nothing you have made can be destroyed by a single
    click, a crashed worker, or a background job you never saw run.

    ⚠️ Two migrations — e3f4a5b6c7d9 (wiki entry soft delete) and
    f4a5b6c7d8ea (campaign soft delete). Both add nullable columns and partial
    indexes only; no data is rewritten, and both were verified to apply, roll back
    and re-apply cleanly. For a major upgrade take a snapshot first — make migrate-safe does the preflight, backup and migrate in one step.

    The bot contract is unchanged (still v1), so you can upgrade the backend and
    bot images independently if you need to. Everything added to /api/bot/* this
    release is optional on both sides.

    ⚠️ Deleted things now occupy disk for 30 days. Campaigns and wiki entries go
    to a trash rather than being removed immediately, and are purged by a daily job
    once the grace period elapses. The window is enforced with a 7-day floor in
    code — configuring a shorter one is clamped up, deliberately, because these are
    artefacts nobody can reconstruct.

    New optional setting: RESTART_POLICY in .env. Leave it unset unless
    something else on your host owns boot ordering; see .env.example.

    Added

    • [webapp] The container restart policy can now be set per host. A new
      optional RESTART_POLICY in .env. Left unset it behaves exactly as before
      — the stack comes back on its own after a reboot — so no existing install
      needs to change anything.

      Set it to on-failure only if something else on your host is responsible for
      starting stacks in order, such as a boot sequencer pacing many Compose stacks
      across shared disks. It still restarts containers that crash; it only stops
      Docker bringing the stack up by itself when the daemon starts.

    • [webapp] Deleting a campaign can now be undone, and is much harder to do by
      accident.
      Deleting a campaign removed everything in it — every session,
      transcript, summary, wiki entry, note and ledger entry — immediately and
      permanently, and the only thing standing in the way was a browser dialog
      that any GM on the campaign could dismiss on reflex. There was no undo, no
      pause, and nothing suggesting you take a copy first.

      Deleting now asks you to type the campaign's name, offers to export the
      campaign before you go ahead, and puts it in a trash for 30 days rather than
      destroying it. Deleted campaigns appear at the bottom of your dashboard
      with a countdown, and restoring one brings back everything exactly as it was.
      Nothing is deleted for real until the 30 days are up.

      Archiving is unchanged and still means what it always did — a campaign moved
      out of the active list that you can still read, browse and export.

    • [webapp] Deleting a wiki entry can now be undone. Deleting an entry used
      to remove it permanently and immediately — along with its entire edit
      history, which went in the same instant. A GM who deleted what looked like a
      duplicate, and realised afterwards it was the real entry with months of lore
      in it, had nothing at all to go back to.

      Deleted entries now sit in a trash for 30 days, where a GM can put them back
      with everything intact. Admin → your campaign's wiki trash lists what is
      in there and when each one stops being recoverable.

    • [webapp] A wiki entry can be rolled back to an earlier version. The edit
      history has always been visible and never usable: recovering an older draft
      meant copying it out of the history view and pasting it back in by hand, and
      the sidebar and timeline fields could not realistically be restored at all.
      There is now a restore button on each version.

      Restoring saves the current state first, so if you restore the wrong version
      you can simply restore your way back.

    Changed

    • [bot] Recordings now take a sixth of the disk space they used to. The bot
      was storing audio in Discord's format and converting it after the session
      ended. It now converts as it records, so a six-hour session occupies about
      0.7 GB per speaker instead of 4.2 GB, and there is no conversion step to wait
      through when you stop recording.

      Audio quality is unchanged — the conversion filters the audio properly on the
      way down, the same as the old post-processing step did. Recordings made by an
      older version are still readable, and are converted the way they always were.

    Added

    • [webapp] Backups can now leave the machine they are protecting. Until now
      the scheduled backup wrote a database dump to a folder on the same host and
      disk as the database itself. Losing that host — a failed disk, a
      deprovisioned server, ransomware — took the database, the recordings, and
      every backup of the database at the same moment.

      Set an off-host destination in Admin → Backups and each backup is copied
      there as well. It uses rclone, so the same setting works for S3, Backblaze,
      a NAS over SFTP, and most other storage; your storage credentials live in
      rclone's own configuration and are never stored by Quest Board.

      A backup that cannot be copied off-host is now reported as failed, even
      though the dump itself worked. A backup sitting on the disk it was supposed
      to protect is not a backup, and calling it a success is how an instance finds
      out it has none at the worst possible moment. The local copy is still kept.

    • [webapp] Recordings can be included in backups. A database dump cannot
      contain audio files. Switch on Include audio recordings and each backup
      gains a compressed archive of them.

      It is off by default because recordings are far larger than a dump — check
      your disk space before enabling it. But if you keep recordings at all, you
      probably want it: a session whose transcription failed has no transcript, so
      its audio is the only record that the session happened.

    Fixed

    • [webapp] Granting admin by email no longer crashes when you have two
      accounts.
      Accounts are identified by which login provider you came from,
      not by your email address, so the same person can hold more than one — most
      commonly after copying a production database onto a test machine, where your
      test login is a separate account that was never made an admin.

      make set-admin EMAIL=… used to fail with a raw database error in exactly
      that situation. It now lists the matching accounts, shows which one already
      has admin, and tells you how to pick the right one. Nothing is changed until
      you do. The operations guide has a section on this, since it looks like lost
      access when it is not.

    • [docs] The operations guide told you to leave recordings out of your
      backups.
      It described the audio volume as scratch space that "does not need
      to survive a restore". That stopped being true some time ago — recordings
      stay until a GM approves them, a campaign can be set to keep them forever,
      and a failed transcription leaves the audio as the only copy of anything.
      Anyone who followed that advice would have lost every recording on the
      instance during a restore. The guide now describes what the volume actually
      holds, and how to back it up and restore it.

    Changed

    • [webapp] Build tooling: @vitejs/plugin-react 6.0.5 → 6.1.1. Purely a
      build-time dependency; nothing about the app itself changes.

      The update had been stuck since it was first offered, because 6.1.1 declares
      an optional peer whose own optional peer wants Babel 8 while the project is
      on Babel 7 — enough for npm to refuse the update even though none of those
      packages are actually installed. A single overrides entry stops npm chasing
      that chain, which unblocks it without dragging in 61 unrelated dependency
      bumps as the alternative would have.

    Fixed

    • [bot] A recording interrupted by a crash now finishes itself on the next
      start.
      If the bot went down mid-session, the audio it had already captured
      was left as raw working files, and there was no way to get it to Quest Board
      short of someone converting it by hand — the earlier fix stopped that audio
      being deleted, but stopped there. A recording that was interrupted an hour
      into a four-hour session sat on disk indefinitely.

      On its next start the bot now converts what it captured and hands it over for
      transcription, and the session appears as normal. Nothing is reconstructed:
      the tracks were already recorded against a single session clock, so the
      timings and who-said-what are the real ones. Speakers are named the way they
      are on any other recording — server nickname first, so a player whose
      nickname is their character's name still shows up as that character.

      It errs towards leaving the audio alone. If it cannot reach Quest Board, or
      the session's group has no Discord server attached, it changes nothing and
      tries again next start. If the session already has a transcript it keeps the
      take but will not overwrite what is there.

    Added

    • Recordings left behind by a deleted campaign are now reclaimed. Deleting
      a campaign removed its sessions but left every recording on disk, and nothing
      ever cleaned them up — one sweep only looks at sessions that still exist, and
      the other treats a handed-off recording as permanently off-limits. On a busy
      instance that is storage that only ever grows.

      A daily job now removes recording folders that no session refers to, after
      giving each a week's grace, and records what it removed in the audit log.

      It is deliberately cautious about it: it only ever touches folders it
      recognises as its own, it leaves anything recent alone, and if the session
      list comes back empty it refuses to do anything at all — an instance with
      recordings on disk and no sessions has a database problem, and treating that
      as "everything is an orphan" would delete every recording on the volume.

    Added

    • The bot event queue's size is configurable, and reports whether it is big
      enough.
      Notifications for the bot wait in a queue capped at 10,000 — enough
      for one group, but every group on an instance shares it. A bot down long
      enough for its unread notifications to age out loses them; the queue is what
      makes delivery reliable, so there is no catching up afterwards.

      BOT_EVENTS_MAXLEN now sets that cap, and a new
      bot_events_trimmed_unread_total metric reports whether notifications are
      actually being lost — so the number can be sized from evidence instead of
      guessed at. It reads nothing rather than zero when there is not yet enough
      data to tell, since "nothing to measure" should not look like "nothing
      wrong".

    Fixed

    • Unsaved writing is much harder to lose. Three surfaces could discard
      work with no warning:

      • The Session Shelf's tonight-notes — the field you type into during
        play — only saved when it lost focus. Closing the tab with the cursor
        still in it lost everything since the last time you clicked away. It now
        saves as you type, a moment after you stop.
      • The Beat Planner discarded your draft when you switched sessions in
        its own dropdown. It now asks first.
      • Session summary and transcript editors, the Beat Planner and the Shelf
        all now warn before a tab close or reload with unsaved text.

      Note that the browser shows its own generic "Leave site?" wording for that
      warning — it does not let an app supply the message. Warning on in-app
      navigation needs a change to how the app routes pages and is tracked
      separately.

    Fixed

    • [bot] A vote that doesn't reach Quest Board now tells you. Reacting to a
      poll in Discord leaves your emoji on the message whether or not the vote was
      actually recorded — so a brief backend outage during a voting window meant
      several people believed they had voted when they hadn't. Voting would then
      close on the tally that happened to arrive, confirm a time, and nobody would
      ever learn a vote had gone missing.

      A reaction that fails to register now gets you a DM saying so and what to do,
      while there is still time to fix it. If your DMs are closed, the bot marks the
      message with ❗ instead. Successful reactions stay silent.

      The same applies to RSVPs and summary feedback, and to reactions on a poll
      Quest Board has stopped tracking (an old poll, or a bot restart) — previously
      indistinguishable from reacting to any other message, and equally silent.

    Fixed

    • A departing member's shared notes are kept when someone else has already
      left.
      Deleting an account anonymises what you wrote onto a shared "Deleted
      user" record. If a second person later deleted their account and had written
      on the same session, their note collided with the first one and was thrown
      away instead — so the campaign quietly lost one person's writing, in a way
      indistinguishable from it never having been written.

      Both accounts' notes are now kept together on that session.

      Votes and attendance still cannot be merged — one vote per person is the
      point of the rule, and there is only ever one "Deleted user" — so a second
      departure genuinely does reduce those tallies by one. That was already true;
      what is new is that it is now counted and recorded in the audit log rather
      than happening silently.

    Fixed

    • The campaign journal works for campaigns with more than one GM. It
      errored outright for every member of a co-GM campaign — the page looked as
      though everyone's notes had vanished, when in fact it was crashing while
      loading them. Co-GMs are supported everywhere else in the app; this one query
      assumed there was exactly one.

      Both GMs' public notes now appear, each labelled with who wrote it, so two
      people's notes under one "GM Notes" heading no longer read as one person
      contradicting themselves.

    Fixed

    • A queue hiccup no longer leaves lore generation stuck at "queued…". The
      session was marked queued before the job was actually handed to the queue, so
      a momentary blip left it claiming work that nobody would ever pick up — and
      because it looked busy, re-running it was refused. The failure is now
      reported straight away, as a failure, with the re-run available immediately
      rather than after a wait.

    Added

    • You are told before a recording is deleted for good. Audio reaches
      deletion through the trash, and until now nothing announced that the clock
      was running — the countdown existed only in the admin console, so noticing
      required knowing to go and look there.

      Quest Board now posts to your campaign's Discord channel a few days out,
      saying which recording, exactly when, and how to keep it. Once per trip
      through the trash, not daily: a message that arrives every morning is one you
      stop reading. The same countdown appears on the session page itself.

      It says explicitly when a session's transcription failed, because that is
      the case where the recording is the only copy of anything — no transcript was
      ever produced — and losing it loses the session rather than just its media.

    Fixed

    • "Append to beat notes" no longer wipes them. The Workbench's session-prep
      tool composed its append against a field the campaign session list has never
      returned, so the existing notes always read as empty — the generated sheet
      replaced whatever the GM had written, and the tool reported "Appended to
      beat notes."
      A GM with an evening of hand-written prep lost it, and was told
      the opposite. There is no undo for beat notes.

      The same root cause left the beat-notes editor opening blank no matter
      what was stored, so typing there and saving replaced the stored notes too.

      Beat notes now have a read path and both places use it. If the current notes
      cannot be loaded, appending fails and says so rather than composing against
      nothing — a button labelled Append must never be able to replace. The notes
      stay GM-only: the read is gated the same way the transcript is, so a player
      cannot see the GM's plan for the evening.

    • Nothing the app generates will overwrite what you wrote. Two paths did,
      both silently, and neither keeps history — so the work was not merely
      replaced, it was gone.

      The campaign storyline was rebuilt from raw session summaries every time
      any session in the campaign was transcribed, re-transcribed, or had its
      summary touched. An hour spent turning AI summaries into a readable chronicle
      survived until the next recording. A hand-edited storyline is now left alone;
      the "Regenerate" button still rebuilds it, because that is you asking.

      Picking a canonical name — settling what to call a tavern — re-summarised
      the whole session and replaced the summary with it. A GM who had corrected
      the AI's mistakes by hand got those mistakes back, from an action that only
      claimed to name something. Naming no longer touches the summary at all; the
      name still applies to everything generated afterwards, which is what it was
      for.

      The two compounded: one canonical-name pick could destroy a corrected summary
      and the campaign chronicle, because the first rewrite triggered the second.

    Fixed

    • Two people editing the same thing no longer silently erase each other.
      Notes, summaries and planning notes were saved last-write-wins with no check
      of any kind, so whoever clicked Save second won and the other edit was gone —
      no error, no warning, nothing in a log.

      The case that actually bit: you open a session on a laptop, type notes from
      your phone over Discord all evening (which correctly appends them), then click
      Save on the laptop out of habit at the end of the night. The laptop still held
      the note as it looked before the session, and saving replaced the whole
      evening with it.

      Saves now carry the version they were based on. If the thing changed
      underneath you, the save is refused and you are told to reload rather than
      finding out later that something is missing. Nothing is auto-merged and
      nothing you typed is thrown away — you choose what to keep.

      Appending instead of replacing was the other option and is worse for a text
      editor: fixing a typo would duplicate the whole note rather than correct it.
      Refusing the write loses nothing either way.

    Added

    • Four actions that could only be taken once can now be taken back.
      Marking a session complete, cancelling one, approving a summary and having a
      wiki draft stall were all one-way doors: no endpoint could reverse them, so a
      mis-click or a lost worker left editing the database by hand as the only
      repair.

      • Undo complete and Reinstate return a session to where it was. A
        cancelled session goes back to confirmed if it still holds a time and to
        proposed if it does not. Both are recorded in the audit log, because they
        rewrite something players and Discord have already seen.
      • Withdraw approval lets a GM correct a summary and publish the
        correction. Approving is what posts the summary to Discord, and it could
        only happen once — so an error spotted after approval was permanent in the
        channel no matter how many times the text was fixed in the app. Quotes are
        unpublished from players while the approval is withdrawn, mirroring what
        approving did.
      • A wiki draft stuck generating can be released without discarding it.
        Discarding was the only exit and it deleted the draft outright, taking
        every inline edit and every round of iteration with it. Releasing marks the
        draft failed and changes nothing else, which is enough — iterating,
        approving and discarding are all reachable from there. Guarded so a draft
        that may still be running is left alone.

      What none of these do is pretend the side effects never happened. A Discord
      message already delivered is not recalled, shelf notes already merged into a
      wiki entry's GM notes stay merged, and withdrawing an approval does not pull
      the audio back out of the trash — that grace window is deliberately bounded
      (see the retention change in v4.0.1) and an approve/withdraw cycle must not
      be able to extend it.

    Fixed

    • [webapp] A stalled lore run no longer hides its own retry button. The
      re-run action was hidden for every in-progress state, so a run whose worker
      died left the session showing "Extracting lore candidates…" with nothing to
      click. Once a run has gone half an hour without any sign of progress, the
      action comes back, labelled so it is clear the run is being replaced rather
      than queued alongside. A run that is merely slow keeps reporting progress at
      each stage and is left alone.

      Closing the same hole from the other side: the endpoint behind that button
      refused a second run only while the first was queued, not while it was
      extracting or matching. Triggering it during either of those stages started a
      second extraction over the same transcript — duplicate proposals, and
      double the tokens. Nothing hit it in practice only because the button was
      hidden, which is exactly the rule being relaxed here, so both halves ship
      together.

    • [webapp] A restart during processing no longer strands a session forever.
      Celery acknowledged a job the moment it was received, so a worker that died
      mid-run took the job with it — nothing was redelivered and nothing was logged.
      The session had already been flipped to "Processing…" on intake, and both
      retry buttons refuse to act on a session in that state, so the only way out
      was an UPDATE against the database by hand. An ordinary
      docker compose up -d --build during a three-hour transcription was enough to
      cause it.

      Work is now acknowledged on completion, so the queue redelivers whatever a
      dying worker was holding. Two safeguards ship with it, because redelivery
      alone would turn a job that kills its worker into a job that kills every
      worker in turn: a six-hour limit (above the six-hour recording maximum, so a
      legitimate full-length transcription is never cut off) converts that into an
      ordinary failure, and each worker now reserves one long job at a time rather
      than a batch.

      An hourly watchdog is the backstop for anything redelivery cannot reach.
      A job with no progress for eight hours is marked failed with a plain
      explanation, which puts the retry button back within reach; audio, lore
      extraction, entry drafts and Workbench generations are all covered. Nothing is
      deleted, the audio path is left intact, and a job still legitimately running is
      left alone. Transient network failures during transcription now retry twice on
      their own before any of this applies.

    • [bot] The bot no longer deletes recordings it told you were saved. Its
      startup cleanup removed any session directory that was unmarked and more than
      an hour old, and the "this is safe" marker was only written after a
      successful upload. So a failed upload, a crash around the moment of upload, a
      crash mid-recording, or the backend declining to overwrite an existing
      transcript all left a real recording looking like abandoned scratch — and the
      next restart destroyed it. In two of those cases the bot had just told the GM
      in the channel that the recording was safe.

      Deletion now requires proof there is nothing to lose: anything holding a
      non-empty track is kept, marked or not, and reported in the log instead. A
      recording whose upload failed is also retried automatically on the next
      start
      , replaying the original request rather than a reconstruction, so it
      usually repairs itself without anyone noticing.

      Raw capture files from a crash mid-session are likewise kept rather than
      deleted. They still need finalising by hand — the log names the path — but
      they are no longer thrown away.

    Changed

    • Scheduled database backups are now on by default, and land somewhere you
      can see.
      They shipped disabled, which meant an instance only had backups if
      its operator went looking for the setting — production ran for months with no
      scheduled dump ever taken. They now run nightly out of the box, writing to
      ./backups beside your docker-compose.yml instead of a Docker named
      volume buried under /var/lib/docker. That is the difference between a file
      your own backup tooling already sweeps up and one it never sees. An install
      that has deliberately turned backups off keeps that choice.

      Upgrading: your existing dumps are still in the quest-board_backups
      volume. Nothing moves or deletes them — docs/OPERATIONS.md has a one-liner
      to copy them across when you are ready.

      Still not covered: the media and audio volumes, and any off-host copy. A
      backup on the same disk survives a bad migration, not a dead machine. Point
      your own tooling at ./backups in the meantime.

    Downloads
  • v4.0.1 db77a4e657

    v4.0.1
    All checks were successful
    CI / Frontend tests, audit, and build (push) Successful in 1m18s
    CI / Backend lint (ruff) (push) Successful in 33s
    CI / Docker image build (push) Successful in 13s
    Release / Create Forgejo release (push) Successful in 19s
    CI / Bot/backend version sync (push) Successful in 59s
    CI / Summarisation accuracy eval harness (stub provider) (push) Successful in 2m41s
    Release / Build and push versioned images (push) Successful in 1m26s
    CI / Backend migration, tests, and audit (push) Successful in 4m54s
    CI / Bot tests and audit (push) Successful in 4m38s
    Stable

    claude-bot released this 2026-08-29 07:01:42 +00:00 | 2724 commits to main since this release

    Raw audio is never destroyed without a recovery window, and the secrets at
    rest are both readable and actually encrypted.

    No migrations. Nothing to rehearse, nothing to back out.

    ⚠️ Rebuild, do not pull. The backend image changes (it now pins its
    PostgreSQL client to the database's major version), so deploy with
    docker compose up -d --build. A pull-and-restart will not apply it.

    ⚠️ Audio now occupies disk for longer. Every retention mode reaches
    deletion through the trash, so audio survives its retention window plus the
    trash grace (minimum 7 days) rather than being removed the moment the window
    expires. Under the previous default it was deleted during processing and
    occupied nothing at all.

    If the startup log names a setting as having been stored in plaintext,
    rotate that credential.
    Re-encrypting protects it going forward; it does not
    un-expose a value that was already readable in every backup taken until then.

    Security

    • Sensitive settings stored in plaintext are now encrypted on startup.
      Encryption only ever happened on write, and rows written before it was
      introduced were passed through on read forever — so a credential that had
      never been rotated since could still be sitting in clear text in the database
      and in every backup, with nothing reporting it. Any such row is re-encrypted
      at startup and logged by name. If a row is reported, rotate that credential
      — it was readable in every backup taken until now.

    Fixed

    • Database backups are restorable again. The backup ran pg_dump from the
      backend image while a restore runs pg_restore against the database image,
      and the two were on different major versions — so every dump was rejected by
      the server's own tooling with unsupported version (1.16) in file header,
      while the job reported success and recorded it as one. The backend image now
      pins its client to the database's major version, a test asserts the two stay
      in step, and the backup refuses to run rather than writing a dump it knows
      cannot be restored. Existing dumps written before this are not restorable
      without a matching newer client
      — replace them by taking a fresh backup
      after upgrading.
      crashes on first use.** Sensitive settings are encrypted with a key derived
      from SECRET_KEY, which lives in .env and is not part of a database dump —
      so restoring elsewhere left them unreadable, and reading one raised a raw
      cryptography error out of the settings layer instead of taking the
      "not configured" path every caller already handles. Unreadable settings now
      degrade to unconfigured, are reported once and clearly, and are listed at
      startup so the problem is found during the restore rather than days later
      when a session fails to summarise. docs/OPERATIONS.md now states that
      SECRET_KEY must be backed up alongside the database.

    • Admin can distinguish "never configured" from "configured but
      unreadable".
      They render identically in every settings form, which is how a
      restore produced a system that looked complete and was not.

    • Raw audio is no longer deleted the moment a transcript exists. The
      recommended retention mode destroyed the per-speaker WAVs inline during
      processing, on the reasoning that there was nothing left to reprocess. That is
      wrong in the case that matters, because the pipeline itself changes: v4.0.0
      rewrote transcription, extraction, validation and composition, and no real
      recording survived to validate any of it against. The mode is now
      trash_after_processing — the audio moves to the trash, stays recoverable
      there, and is deleted only once the grace has elapsed. The old
      delete_after_processing name is still accepted from existing configuration
      and normalised on read, so no action is needed on upgrade.

    • The trash grace is a floor, not a default. A configured value below seven
      days is clamped up, so a misconfiguration cannot reintroduce same-day loss.

    • An expiring retention window no longer deletes directly. Under
      retain_days the sweeper now moves expired audio to the trash rather than
      removing it, so every mode reaches deletion through the same recoverable
      window. Audio therefore lives for the configured window plus the trash
      grace
      — budget disk accordingly.

    • A trashed recording is no longer deleted early by the retention window.
      The grace-period sweep and the policy sweep raced, and the shorter one won: a
      recording trashed today, whose window had already passed, was removed on the
      next scheduled run without its grace ever being observed.

    • A silent speaker is no longer reported as having spoken. The fix that was
      supposed to report a backend-dropped silent track as present-but-uncaptured
      was overridden, because the same person was still counted among the captured
      speakers and that assignment won. It only affected recordings the current bot
      did not produce — which is precisely the case the guard existed for.

    Downloads
  • v4.0.0 23f5a1b671

    v4.0.0
    All checks were successful
    CI / Frontend tests, audit, and build (push) Successful in 1m17s
    CI / Backend lint (ruff) (push) Successful in 37s
    CI / Summarisation accuracy eval harness (stub provider) (push) Successful in 1m16s
    CI / Docker image build (push) Successful in 17s
    Release / Create Forgejo release (push) Successful in 18s
    CI / Bot/backend version sync (push) Successful in 40s
    CI / Bot tests and audit (push) Successful in 2m2s
    Release / Build and push versioned images (push) Successful in 2m44s
    CI / Backend migration, tests, and audit (push) Successful in 9m18s
    Stable

    claude-bot released this 2026-08-28 23:56:11 +00:00 | 2729 commits to main since this release

    The summary is no longer something the model asserts. It is composed from events
    that have each been checked against the transcript in code — so chronology is a
    sorted() and attribution is a lookup, not a model behaviour.

    Six migrations (a2b3c4d5e6f8, b3c4d5e6f7a9, c4d5e6f7a8b0,
    d5e6f7a8b0c1, e6f7a8b0c1d2, f7a8b0c1d2e3), applied in order by the
    migrate service. All additive. Only one backfills: c4d5e6f7a8b0 copies each
    member's existing character into the new campaign_characters table and leaves
    the old columns in place, so a downgrade is two DROP TABLEs and no data is at
    risk. Take a snapshot before upgrading anyway — six at once is more than
    usual, and the automatic pre-migration backup only helps if you know it is there.

    Rehearsed against a copy of production data before release: all six apply
    cleanly, row counts unchanged, backfill exact.

    BOT_CONTRACT_VERSION stays 1, so you can upgrade one image at a time.
    BOT_EXPECTED_APP_VERSION moves to 4.0.0, which only logs a warning on
    mismatch. Upgrading both together is still recommended.

    If you run with VAD disabled, transcription request volume changes. That
    path used to send every speaker's track in one /transcribe/session request;
    it now sends one request per speaker to /transcribe, the same endpoint the
    VAD path already uses. Attribution can no longer depend on a label the server
    echoes back — see below. VAD is on by default, so most installs are unaffected.

    Nothing is backfilled into the new structure. Transcript segment rows,
    beats and summarisation runs exist only for sessions recorded after upgrading.
    Old sessions keep working; they simply have no provenance to show, and a
    plausible reconstruction would look authoritative without being so.

    Added

    • Structured, verified session summaries. Extraction emits timestamped
      events with cited transcript lines, constrained by a decoding grammar where
      the provider supports one. A pure-code validator then checks every citation
      resolves, every claimed actor actually spoke a cited line (or was named by the
      GM in one), and every event's time range agrees with its own evidence. Only
      events that pass reach the prose. The check costs nothing and behaves
      identically on a 9B local model and a frontier hosted one, which is what keeps
      the accuracy floor independent of model size.
    • The events behind a summary are kept and readable. GET /api/sessions/{id}/summarisation-runs (GM only) returns each run with its
      beats, their cited lines resolved to text, which failed validation and why,
      what the validator repaired, and timestamp anchors back into the transcript. A
      reprocess appends a run rather than replacing one, so two summaries of the same
      audio can be compared.
    • Transcript segment rows. One row per utterance, with the track owner on it.
      Member erasure, lore chunking and per-speaker QA read these instead of
      re-parsing the rendered text, so the display format stops being a data
      contract. GET /api/sessions/{id}/transcript-qa/speakers reports per-speaker
      segment counts and spoken duration.
    • A player can run several characters (campaign_characters), with a
      per-session override so correcting a mid-campaign swap does not relabel old
      recordings when they are reprocessed.
    • A per-campaign spoken language. Unset means detect, which stays the
      default — a default of English would silently mistranscribe every non-English
      table until someone found the setting.
    • A declared context window per provider, set in Admin → Bot Settings, used
      to size every prompt in the pipeline.
    • A golden-transcript eval harness (python -m evals) scoring coverage,
      attribution, chronology and validation rate, with --repeat because one run
      of a sampled pipeline is not a measurement.
    • Admin → summarisation mode, a one-release escape hatch back to the
      pre-4.0.0 prose path if the verified one misbehaves on your data. It defaults
      to the verified path and logs a warning on every session summarised without
      checks.

    Fixed

    • A session that ran long on the clock but short on talking was thrown away.
      Every track is padded with silence to the full length of the recording, and
      nothing stops a recording when the voice channel empties — so a table that
      played for two hours and forgot to stop for another fifty produced a complete,
      correctly attributed transcript that the coverage check then rejected for
      covering "only" 58% of the recording. It was measuring speech against wall
      clock, which is not a coverage measure of anything. Worse, it ran after
      transcription, so the work was already done, and it failed identically on
      every retry while blaming the capture clock. Coverage is now measured against
      how far into the session the tracks actually carry audible speech.
    • A second recording destroyed the first one's work. A GM who stopped at the
      break and started again overwrote the transcript, any edits made to it, every
      curated highlight, and the source audio — so no reprocess could recover it.
      Intake now refuses with a clear message, curated highlights survive a
      reprocess, and a second take is stored beside the first instead of over it.
    • A quiet player could be deleted from a full-length session. The silence
      guard sampled 32 seconds of a 77-minute track, so it only reliably detected a
      speaker whose audio contained one unbroken stretch longer than ~145 seconds. A
      player who says a few words every several minutes fell between the probes and
      their entire track was dropped before transcription, with the pipeline
      reporting success. Detection is now an exhaustive scan, and a speaker dropped
      by the backend is reported to the GM as present-but-uncaptured rather than
      vanishing into a log.
    • The transcription server's echoed speaker label could silently relabel a
      whole track.
      Attribution is now taken from the file that was sent and never
      from the response, on every path.
    • Ollama installs summarised a fraction of each session. num_ctx was only
      set when a caller happened to pass a window, and almost none did, so requests
      fell back to Ollama's 2-4k default and were silently truncated. Every request
      now declares one.
    • A summary cut short by the model's output limit looked complete. It is
      still returned — half a summary a GM can read and correct beats none — but the
      truncation is now recorded and surfaced.
    • Constrained JSON output was doing nothing on llama.cpp. The documented
      request shape is silently ignored by real servers; the working form is now sent
      alongside it, and a response that needed repair is recorded as degraded.
    • Structured extraction sampled at random, so summary coverage varied up to
      eightfold run to run on the same recording. It is greedy-decoded now.
    • Compose silently discarded about a quarter of the verified events it was
      given. Missing events are detected and re-requested by name.
    • Quotes on the quote board carried a speaker and timestamp the model
      asserted
      ; both now come from the utterance the words were found in.
    • Four prompts grew without limit — /ask in particular assembled every
      approved lore entry at full length, forever.
    • Erasing a member matched their name in the transcript text. It now works
      from the track owner recorded per utterance.
    • A bare Discord ID could appear as a speaker name and be read as a person.

    Changed

    • Speaker resolution happens entirely in code before the model sees anything.
      The prompt no longer asks it to rename players to characters or to guess which
      GM lines were an NPC.
    • Long transcripts are summarised in ordered windows sized from the declared
      context window, rather than sent whole and hoped for.
    • Same-speaker runs are merged before prompting, cutting prompt size
      substantially without losing attribution.
    • Overlapping speech renders as a time range, but only on the lines that
      actually overlap.
    • "No LLM configured" now behaves the same way everywhere instead of eight
      different ways.
    Downloads
  • v3.11.4 6ee2bb9118

    v3.11.4
    All checks were successful
    CI / Backend lint (ruff) (push) Successful in 26s
    CI / Bot/backend version sync (push) Successful in 23s
    CI / Docker image build (push) Successful in 11s
    CI / Bot tests and audit (push) Successful in 2m27s
    CI / Frontend tests, audit, and build (push) Successful in 2m36s
    CI / Backend migration, tests, and audit (push) Successful in 6m9s
    Release / Create Forgejo release (push) Successful in 12s
    Release / Build and push versioned images (push) Successful in 21s
    Stable

    rbrooks released this 2026-08-07 06:18:26 +00:00 | 2828 commits to main since this release

    Silent failures made visible, and a frontend toolchain that had been building
    on an end-of-life runtime.

    No migrations; BOT_CONTRACT_VERSION stays 1, so the backend and bot images can
    be upgraded independently. Self-hosters who build their own images will pick up
    Node 22 for the frontend stages automatically — nothing to change by hand.

    Changed

    • [webapp] Migrated the router from react-router-dom 7 to react-router 8.
      In v8 the react-router-dom package was folded into react-router itself and
      is no longer published, so staying on 7.x meant staying on a line that will
      receive no further fixes. This also clears the last allowlisted advisory
      (GHSA-qwww-vcr4-c8h2): the allowlist is now empty, so the audit gate is a
      real check rather than a formality. No behaviour change — every routing API in
      use is exported unchanged from the new package. (#284)

    • [webapp] The frontend is now built and tested on Node 22 instead of Node 20.
      Node 20 reached end-of-life on 2026-04-30 and no longer receives security
      patches, so the production frontend image was being built on an unsupported
      runtime. CI and both Dockerfile frontend stages move together, deliberately —
      testing on one major while building the image with another is how a
      runtime-only break ships green. This also unblocks #284, since react-router v8
      requires Node ≥ 22.22.0. (#284)

    Fixed

    • [webapp] Approving a wiki proposal with no working LLM quietly produced a
      worse entry.
      When an LLM was unavailable, approving a proposal that updates
      an existing entry pasted both versions together instead of merging them, and
      said nothing — the result was indistinguishable from a successful merge. It
      still falls back to appending, since that beats refusing the approval, but it
      now records that it did. This also covers a misconfigured endpoint rather
      than merely an unconfigured one: a malformed URL was silently treated as "no
      LLM configured", sending GMs to a settings page that already had a value in
      it. That case is now logged too. (#287)

    • [ops] Docker builds no longer ship the whole working tree as build context.
      The repository had no .dockerignore, so every build of every target sent
      ~226 MB of node_modules plus the full .git history to the daemon. On a
      Windows host it also failed outright, because npm's symlinks under
      node_modules/.bin do not survive the bind mount — making it impossible to
      build the production image locally to verify a change. CI never saw it, since
      a fresh checkout has no node_modules.

    • [webapp] A response the model cut short was silently turned into a partial
      result.
      When an LLM stopped because it hit its output budget, the transport
      returned the half-finished body as though it were complete. The tolerant JSON
      parser then salvaged the longest valid prefix from it, so a reply that should
      have carried twelve wiki proposals could arrive as three — indistinguishable
      from a session that genuinely produced three, and logged as a successful
      recovery. Truncated structured responses are now rejected so the caller fails
      loudly and can retry, rather than persisting a quietly incomplete result.
      Truncated prose is still returned, since it is shortened rather than
      corrupted. Covers all four providers — llama.cpp, OpenAI, Anthropic and
      Ollama, each of which signals this condition differently. (#293)

    Downloads
  • v3.11.3 e5059aa82a

    v3.11.3
    All checks were successful
    CI / Docker image build (push) Successful in 18s
    CI / Frontend tests, audit, and build (push) Successful in 1m17s
    Release / Create Forgejo release (push) Successful in 34s
    CI / Bot/backend version sync (push) Successful in 2m23s
    Release / Build and push versioned images (push) Successful in 54s
    CI / Backend lint (ruff) (push) Successful in 2m32s
    CI / Bot tests and audit (push) Successful in 4m41s
    CI / Backend migration, tests, and audit (push) Successful in 8m24s
    Stable

    claude-bot released this 2026-08-06 17:04:39 +00:00 | 2839 commits to main since this release

    Wiki entry summaries came out as JSON.

    v3.11.2 was tagged but never deployed to production — this bug was caught on
    the dev deploy and supersedes it. Upgrade straight to 3.11.3. No migrations;
    BOT_CONTRACT_VERSION stays 1.

    Fixed

    • [webapp] Wiki entry summaries were generated as JSON instead of prose. The
      summary generator asked the model for "plain prose only" while the underlying
      transport was still in JSON mode with the lore-proposal instructions attached,
      so entries came out with a summary of {"proposals":[]}. The bug predates
      v3.11.2, but was only reachable through the manual "regenerate summary" button;
      v3.11.2 made summaries generate automatically on every approval, which would
      have written JSON into the summary of every entry a GM approved. (#285)

      Existing installs are unlikely to be affected — the generator had no automatic
      trigger before v3.11.2, so only entries where the manual button was used could
      carry a JSON summary. Re-running "regenerate summary" on such an entry now
      produces prose.

    Downloads
  • v3.11.2 f668a6523d

    v3.11.2
    All checks were successful
    CI / Backend lint (ruff) (push) Successful in 30s
    CI / Bot/backend version sync (push) Successful in 23s
    CI / Docker image build (push) Successful in 10s
    CI / Bot tests and audit (push) Successful in 1m20s
    Release / Create Forgejo release (push) Successful in 12s
    Release / Build and push versioned images (push) Successful in 52s
    CI / Frontend tests, audit, and build (push) Successful in 2m48s
    CI / Backend migration, tests, and audit (push) Successful in 7m52s
    Stable

    claude-bot released this 2026-08-06 01:00:07 +00:00 | 2842 commits to main since this release

    The rest of the LLM trigger audit, and a test suite that stops waiting on a
    broker that was never there.

    Follow-ups to the audit that produced v3.11.1: five features that were wired to
    the wrong event, or to no event at all. No migrations, and
    BOT_CONTRACT_VERSION stays 1 — nothing touches /api/bot/*, so images can be
    upgraded independently.

    Fixed

    • [webapp] The session prep sheet only saw the opening of the last session.
      Every recent summary was truncated to a flat 800 characters, against real
      summaries of 1700-2900 — so prep was drafted from roughly the first third and
      never saw where the party ended up. Truncation also kept the beginning,
      discarding the ending, which for prep is the most useful part of a summary.
      The most recent session now gets a near-full budget and, if it ever exceeds
      it, keeps both its opening and its closing. (#271)
    • [webapp] Lore generation could hang forever. If a phase of the multi-pass
      lore pipeline ran out of retries, nothing cleared the session's extracting or
      matching status — the UI showed "generating…" indefinitely, with no error.
      A brief network blip to the LLM host was enough to trigger it. A missing LLM
      endpoint now also fails immediately instead of burning several minutes of
      backoff on a condition that cannot resolve itself. (#287)
    • [webapp] Wiki entries never got their one-line summary. The generator had a
      single caller — the manual "regenerate summary" button — so every entry created
      by approving a bot proposal, promoting a shelf card, or picking a canonical
      name had an empty summary permanently. Summaries are now generated whenever an
      entry becomes canon. (#285)
    • [webapp] Relationship suggestions never saw the session that just happened.
      They were generated the moment the lore pipeline finished, but only consider
      approved entries — and everything just produced was still an unapproved
      proposal. They now run when you approve entries, debounced so working through a
      batch produces one pass rather than one per click. (#286)
    • [webapp] Some GM Workbench generations were missing from history. Three
      paths ran tools without recording them, so those generations never appeared in
      Workbench history and their token usage went uncounted. All inline tool runs are
      now recorded, including failed ones. Caches are unchanged. (#288)
    • [webapp] A queued background job could stall a request. With an
      unreachable Redis, a single enqueue took about two minutes to fail, which
      presented as a hung request rather than a degraded background job. Now bounded
      to under 15 seconds.

    Changed

    • [dev] The backend test suite runs in 36 seconds instead of 16 minutes for
      the same 880 tests. Most of that time was never work — with no broker in the
      test environment, every queued job waited for a connection that could not
      happen. Applies to CI too, which runs Postgres but no Redis.
    Downloads
  • v3.11.1 01c9f86197

    v3.11.1
    All checks were successful
    CI / Frontend tests, audit, and build (push) Successful in 1m54s
    CI / Bot/backend version sync (push) Successful in 24s
    CI / Docker image build (push) Successful in 13s
    CI / Backend lint (ruff) (push) Successful in 42s
    Release / Create Forgejo release (push) Successful in 26s
    CI / Bot tests and audit (push) Successful in 1m52s
    Release / Build and push versioned images (push) Successful in 55s
    CI / Backend migration, tests, and audit (push) Successful in 9m1s
    Stable

    claude-bot released this 2026-08-05 18:32:14 +00:00 | 2851 commits to main since this release

    Approval was unreachable, and the quote board was failing silently.

    Three bug fixes found while investigating why a recurring session was offered
    no title suggestions. One new migration (e0f1a2b3c4d5), additive and with
    a best-effort backfill. BOT_CONTRACT_VERSION stays 1 — no /api/bot/* change,
    so images can be upgraded one at a time.

    Fixed

    • [webapp] GM approval was unreachable on a default install. sessions. audio_processing_status conflated where the audio file is with whether the
      GM had signed off on the content. Under the default retention mode
      (delete_after_processing) the status jumps straight to approved — which
      means "audio deleted", not "GM approved" — so the approve endpoint, which
      required ready, could never be called. Everything gated behind it was dead:
      publishing session highlights to players, reposting the GM-corrected summary
      to Discord, journal entries, and next-session title suggestions. Content
      approval now has its own state and its own endpoint,
      POST /sessions/{id}/approve, gated on having a summary rather than on audio
      state. POST /sessions/{id}/audio/approve remains as a deprecated shim.
      Transcript editing and the feedback tally were gated the same way and are
      likewise fixed. (#278)
    • [webapp] Post-summary tasks fired from three places that didn't know about
      each other
      , so a hand-edited summary rebuilt the campaign storyline and
      nothing else. Replaced with a single fan-out that distinguishes the initial
      transcription from a later edit — the recap email and lore extraction no
      longer re-fire because a GM fixed a typo. (#278)
    • [webapp] Title suggestions generated from nothing, and destroyed
      themselves on failure.
      The task read context from completed sessions but
      runs while the session is still confirmed, and on any LLM error it
      overwrote a good suggestion set with an empty one plus a fresh timestamp.
      (#278)
    • [webapp] The quote board was silently empty. Endpoints that accept
      response_format: json_object without enforcing it can return structurally
      invalid JSON, which the strict parser rejected — logging the result as
      "0 highlights", indistinguishable from the model finding nothing memorable.
      JSON parsing is now tolerant of a truncated or over-closed response, shared
      across every structured-output caller, and both repair and total failure are
      logged at WARNING. A related bug cleared existing highlights before checking
      whether the new extraction returned anything, so each retry of a failing
      session destroyed the previous good set. (#279)
    • [webapp] Admin retention warning for "delete after processing" understated
      the consequences; it now tells you to correct the transcript before approving.
      (#278)

    Added

    • [webapp] Title suggestions on the session edit form — sessions created
      from a recurring series are materialized by a background task and never pass
      through the new-session form, so this is the only place they can be titled
      from a suggestion. (#278)
    Downloads
  • v3.11.0 fe082c3e20

    v3.11.0
    All checks were successful
    CI / Docker image build (push) Successful in 15s
    CI / Backend lint (ruff) (push) Successful in 47s
    Release / Create Forgejo release (push) Successful in 18s
    CI / Frontend tests, audit, and build (push) Successful in 1m16s
    Release / Build and push versioned images (push) Successful in 1m11s
    CI / Bot tests and audit (push) Successful in 2m9s
    CI / Backend migration, tests, and audit (push) Successful in 3m26s
    Stable

    claude-bot released this 2026-07-27 19:55:48 +00:00 | 2870 commits to main since this release

    The Session Reference Shelf: everything relevant to tonight, in one place.

    A per-session board the GM keeps open during play. Anything generated in the GM
    Workbench persists on the shelf for the whole session — whether or not it ever
    becomes canon — and existing wiki entries can be pinned alongside it, so there is
    no hunting a large wiki mid-scene. Cards hold the position you drop them in and
    are never auto-reflowed; the shelf is a spatial surface, not a list.

    The whole feature works offline: no LLM and no transcription is required for any
    of it (the transcript-driven layer is v3.12). One new migration
    (d9e0f1a2b3c4), and BOT_CONTRACT_VERSION stays 1 — the one new bot event is
    additive, so a mixed-version bot keeps working.

    Added

    • [webapp] The shelf itself — a per-session board of reference cards backed
      by a new ShelfCard model and GM-only API, with freeform drag-to-place
      positions that never auto-reflow, resizable cards, per-card "tonight" notes,
      and removal. (#239, #240)
    • [webapp] Add to shelf from anywhere — a shared control on every GM
      Workbench generation result and on wiki entries, defaulting to the campaign's
      in-progress or next session. (#241)
    • [webapp] Glance-compressed cards — each card shows a scannable face
      (name, a one-line want or hook, ~3 facts, a voice cue) with full detail one tap
      away, so cards read at arm's length instead of as walls of text. (#242)
    • [webapp] Spotlight / "now playing" — promote a card to a large stage;
      number keys 1-9 or the card's stage button pick it, arrows or [ ] swap, Esc
      clears. The board card never moves. (#243)
    • [webapp] Scene packs — bundle cards into named scenes during prep and swap
      the board's scene in one tap during play. Swapping hides cards, it never moves
      them, so every scene keeps the exact layout you left it in. (#244)
    • [webapp] Reveal to table — show a card to the players on Discord, in Quest
      Board, or both. Each card carries a player-safe layer separate from its
      GM-only content; when you haven't written one, the server derives a
      conservative default from read-aloud text and public summaries only — never GM
      notes or secrets — and refuses to reveal rather than guess when nothing is
      confidently safe. Players get a new "At the table" view showing only what has
      been revealed. (#245)
    • [webapp] Fast retrieval — a Ctrl-K fuzzy quick-switcher over tonight's
      shelf first and then the wiki, plus number-key hotkeys for the cards on
      screen. (#246)
    • [webapp] End-of-session sweep — one pass over every card touched tonight:
      promote it to the wiki (an AI draft cleans and merges it through the existing
      review rail), carry it forward, or discard it. (#247)
    • [webapp] Carry-forward pins + session navigation — pin a card and it rides
      forward onto every later session automatically, as an interval over the
      campaign's session timeline. Wiki cards render live; generated cards stay
      frozen to their snapshot. Completing a session harvests each wiki card's notes
      into that entry's GM notes; cancelling prunes untouched carried cards. Plus a
      session switcher, a Workbench entry point, and "copy from another session".
      (#252, #254)
    • [webapp] Warm cold-open — the shelf opens with a "previously on" recap, the
      plot threads still open, and a count of the cards carried in, so session N+1
      starts warm. The recap steps over cancelled and unsummarised sessions to find
      the last one that actually has something to say. (#248)
    • [webapp] AI story-arc suggester — a GM Workbench tool that reads recent
      sessions, arcs, threads, and wiki entries to propose candidate story arcs in
      the result rail; accepting one creates a CampaignArc. (#236)
    • [bot] Revealed cards post to Discord — a new additive shelf_reveal event
      posts a card's player-safe layer to the campaign channel. The GM-only side of
      a card never leaves Quest Board. (#245)
    Downloads
  • v3.10.0 b0a8e9e19c

    v3.10.0
    Some checks failed
    Release / Create Forgejo release (push) Failing after 19s
    CI / Docker image build (push) Successful in 54s
    CI / Backend lint (ruff) (push) Successful in 1m39s
    Release / Build and push versioned images (push) Successful in 1m16s
    CI / Frontend tests, audit, and build (push) Successful in 2m35s
    CI / Bot tests and audit (push) Successful in 4m55s
    CI / Backend migration, tests, and audit (push) Successful in 7m42s
    Stable

    claude-bot released this 2026-07-22 02:17:42 +00:00 | 2919 commits to main since this release

    GM Workbench: campaign-grounded generation, right where you plan.

    GM Planning becomes a GM Workbench — a unified surface of AI tools that read your campaign's lore, plot threads, and session history to generate material you can drop straight into play or land into the wiki, all through one generation engine with a persistent scratchpad and the existing draft-review rail (nothing auto-overwrites your canon). BOT_CONTRACT_VERSION stays 1; no new migrations beyond the Phase-0 scratchpad table.

    Added

    • Generation engine + scratchpad foundation — a tool registry and one generalized POST …/workbench/{tool}/generate endpoint (sync inline or async via Celery), an extracted LLM transport, and a GenerationResult history/scratchpad. (#134, #136, #138)
    • Read-aloud descriptions, rumor mill, random tables, loot parcels, the flagship session prep sheet, instant NPCs + voice cues, and recurring-series title suggestions — each grounded in your campaign and landing through the draft-review rail. (#141, #144, #147, #149, #158, #155, #191)
    • Two-pane Workbench layout — a grouped tool palette, a center generation surface, and a persistent history rail. (#153)

    Changed

    • "GM Planning" is now the "GM Workbench" — renamed throughout (nav and header), with the eight generator tools collapsed onto one config-driven panel. (#234)
    • Searchable lore grounding — a searchable, type-grouped picker replaces the flat checkbox list, scaling to large campaigns. (#234)

    Fixed

    • Reasoning LLMs no longer break structured generation — json-mode llama.cpp / vLLM calls disable the model's think block (chat_template_kwargs.enable_thinking=false), so reasoning models (e.g. Qwen3) work with the Workbench.
    • "Nothing to convert" is no longer an error — stat conversion on an entry with no mappable stats shows a gentle informational note (nudging you to generate a stat block from scratch) instead of a red failure banner. (#233)
    Downloads