• 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 | 412 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