-
v4.4.0
StableAll checks were successfulCI / Backend lint (ruff) (push) Successful in 35sCI / Docker image build (push) Successful in 34sRelease / Create Forgejo release (push) Successful in 32sCI / Summarisation accuracy eval harness (stub provider) (push) Successful in 1m0sCI / Bot/backend version sync (push) Successful in 1m10sRelease / Build and push versioned images (push) Successful in 1m6sCI / Bot tests and audit (push) Successful in 1m56sCI / Frontend tests, audit, and build (push) Successful in 3m16sCI / Backend migration, tests, and audit (push) Successful in 7m54sCI / Synthetic session harness (no GPU, no LLM) (push) Successful in 17m35sreleased this
2026-09-12 00:01:27 +00:00 | 2308 commits to main since this releaseAdded
-
[webapp] Wiki suggestions wait until you approve the summary, and your
corrections clear them (#610). Suggested wiki updates used to be made once,
automatically, the moment a session finished transcribing — before anyone had
read a word of it. They are built from the summary and the transcript and
never from the events, so nothing you struck, corrected or relabelled could
reach them: a GM who misspoke "You're in Ironroot Hold" could correct the
event and watch the wiki go on proposing Ironroot Hold as a place the party
had arrived at. Now nothing is suggested until you press Approve summary,
which is the moment you have decided the session's record is right; and any
regenerate, restore or hand-edit throws away the suggestions you have not
acted on yet, putting the button back to Suggest wiki updates. Wiki
entries you already approved are never touched by that. If you want
suggestions before approving, the button works exactly as it always did. -
[webapp] Tell the summary what is story, and what it must not leave out
(#607). Two more corrections in "Everything that happened", beside Strike and
Correct. Mark as table note moves an event the model mistook for story —
a rules explanation filed as a reveal — into the folded table notes, where it
is never narrated; a table note that was really part of the story gets Mark
as story, with the kind you pick (other, unless you choose). Emphasise
marks an event the summary must include: it is written in on top of the
events the summary is sampled from on a long session, never in place of one,
and the writing step is told it may not be left out. As before, everything is
held on the page — several corrections on one event if they go together, each
undoable — until Regenerate summary. Afterwards the new summary's events
show a star on what you emphasised and "was: reveal" on what you relabelled,
and Un-emphasise takes a star off. Players see the result — a relabelled
table note folded like any other, an ordinary row for one brought into the
story — and never the markers. -
[webapp] When the summary is wrong because the table said something wrong,
you can fix the event and get a new summary (#424). Some mistakes are true
to the transcript, so nothing can catch them: the GM says "You're in Ironroot
Hold", meaning "you were", the event checks out because it was said, and every
summary since puts the party in Ironroot Hold. Under the summary, in
"Everything that happened", a GM can now Strike an event or Correct its
wording, see the pending changes marked, undo any of them, and then
Regenerate summary. Only the writing step runs again — no transcription,
no re-reading of the session — so it takes about as long as the summary's
last step did. The events the model found are kept exactly as it found them;
the corrected set is recorded alongside them, with the original wording, so a
corrected event shows "Corrected by GM" and what it used to say, and a struck
one stays visible to you, struck through. Players see the corrected text and
never see a struck event. Regenerating replaces the current summary, hand edits
included, so the old one is kept and Restore the summary from before puts
it back. An approved summary can be regenerated just as it can be edited, but
the copy already posted to Discord is not changed — withdraw approval and
approve again to repost. If the regenerate fails, the summary is left as it
was, the reason is shown, and your corrections stay on screen to try again. -
[webapp] A short summary you will actually read, and underneath it,
everything that happened (#568). Long sessions were being summarised badly in
two different directions, and both were the same argument: the summariser is
asked for three to six paragraphs and told to represent every verified event,
and on a real 59-event session the local model resolved that by quietly leaving
twelve of them out, while the hosted models resolved it by writing ten thousand
characters nobody skims. So the summary is now written from up to thirty-six
events (three to seven paragraphs' worth; see #600), sampled across the whole
evening rather than off the top of it, in the order they happened (see #589) —
and every verified event, however many
there are, is listed in full under it: "Everything that happened (59 events)",
collapsed until you want it, then one timestamped line each saying when, who,
and what. That list is built from the checked events themselves rather than
written by a model, so nothing in it is a guess and nothing can be dropped from
it, and it is there for your players too, exactly like the summary it sits
under. Events the validator could not confirm are still yours alone, with the
reason, where they always were. A session under thirty-six events reads exactly
as it did before this release. -
[webapp] You can tell Quest Board what the transcriber calls your
characters, and it will stop calling them that (#579). Every character now
has an "Also heard as" list — the names the transcript gets wrong for them —
and after transcription each whole-word match is snapped back to the real
name, possessives included ("Vic's" becomes "Viq's") and without touching a
word that merely starts the same way ("Victor" is left alone). This is not
another attempt to persuade the decoder: the campaign's names already go to
Whisper as an initial prompt and, since #577, optionally as hotwords, and on
the session that prompted this "Viq" still came back as Vic, Vick or Nick in
all 22 mentions, because a speech model will not overturn a common English
word the audio genuinely matches. So the correction is stated by you and
applied deterministically, to every speaker's track — players say each other's
characters' names constantly, and a line that says "Vic" cannot support a
summary beat about Viq. The listed misspellings also go to the transcriber
alongside the real names, which sounds backwards and is the point: a
consistently wrong spelling is one that can be corrected, and three different
wrong ones are not. Quest Board refuses an entry that is another character's
name, and each run records what it corrected asname_snapson the
summarisation run, so a name that never needed fixing is an entry you can
delete (migration6b7c8d9e0f1a). -
[webapp] The campaign's names can be sent to the transcriber as
hotwords
instead of an initial prompt (#577). The bundled WhisperX server takes the
same list of proper nouns two ways — Whisper'sinitial_prompt, a sentence
the decoder is told it has just transcribed, and faster-whisper'shotwords,
the dedicated bias for exactly this job — and they share one slot in the
prompt, so exactly one is ever sent. Which works better is a question about
your audio, your model and your campaign's names, so it is now a switch:
QB_ASR_VOCABULARY_STYLE=hotwords(ornone, the free control arm, which
does not even query the wiki). Unset isinitial_promptand sends
byte-identical requests to every release before this one, so nothing moves
until you move it. Asking for hotwords from a server that does not declare
them falls back to the prompt and warns on every capability probe rather than
running your comparison on the arm you were trying to leave. Each run records
the style it used in the new nullablesummarisation_runs.vocabulary_style
(migration4f5a6b7c8d9e), exposed asvocabulary_styleon the
summarisation-run responses, so the comparison can be read back from the
database instead of from a memory of when the setting changed. -
[webapp] When a rightsholder writes in, there is now somewhere to put it
and something to do about it (#555). An administrator can record a notice —
who wrote in, how to reach them, what they claim, what they say it is about,
when it arrived, and your own notes — and then act on it across everything on
the instance in one place: disable or remove a pack loaded for the whole
instance, a pack in any campaign including one you did not import, or
individual records inside either. Disabling is immediate and reversible and
takes the content out of every search on the next query; removal is final.
Whatever you do, the person who imported the content is told on the pack
itself, in a message naming the notice and quoting the affirmation they
agreed to at import — the version on their own pack row, not today's wording
— and every action is written to the audit log with the notice id, so the log
can still answer "why did this go?" after the pack itself no longer exists. A
batch of actions is all-or-nothing. There is also one operator-set contact
address for notices, readable by any signed-in user so a licences page can
print it. Quest Board decides none of this. Nothing is taken down
automatically, no notice moves its own status, no counter-notice reinstates
anything, and there is no repeat-infringer logic — whether a claim is valid
and what you are obliged to do about it are yours, and the software only
records and applies what you chose. New migration2d3e4f5a6b7cadds
content_notices; no existing table changes. What an operator actually does
with all this is written up indocs/OPERATIONS.md→ "Handling a
rightsholder notice", and the endpoints are indocs/API.md. -
[webapp] Import a Foundry VTT compendium export (#553). Upload the folder,
a zip of it, or the single JSON array people share — Quest Board reads the
documents either way. Pathfinder 2e and D&D 5e are mapped in full, so their
creatures arrive as stat blocks rather than as prose, and a system Quest Board
has no map for still imports as searchable text with itssystemblock kept
intact for export. HTML descriptions become Markdown, and an actor's own items
become the strikes and abilities in its entry. Foundry keeps a document's
publication and licence in four different places depending on whether it is an
item or an actor and whether the export predates the Pathfinder remaster; all
four are read, because a creature that imports with no licence is the one
failure a content pack cannot afford. -
[webapp] Import the D&D 5e homebrew you already have, from 5etools or
Dungeon Master's Vault (#553). Two more readers for the content-pack
importer, so people with homebrew in the two places most of it lives do not
have to convert it by hand. A 5etools homebrew.jsonbrings in its
monsters as full stat blocks — armour class, hit points, speed, challenge
rating, ability scores, size and type — along with its spells, items,
conditions, feats, traps, variant rules, classes and backgrounds; 5etools'
inline markup is rendered to readable text rather than left as{@hit 7}, so
what you search is what you would read. A.orcbrewexport from Dungeon
Master's Vault or OrcPub2 brings in its monsters, spells, magic items, feats
and character options, computes a monster's hit points from its hit dice when
the file does not state them, and takes the pack's name from the homebrew pack
the entries belong to, since the format has no header to read it from.
Neither format records a licence unless the file itself declares one —
Quest Board does not guess on your behalf. Content in either file that
Quest Board does not recognise is imported as searchable text with the
original kept for export, and it tells you what it did rather than dropping
it quietly; anything it genuinely could not read is reported as a warning on
the import, alongside the ones the importer itself raises. Adds one backend
dependency,edn-format, for reading.orcbrew. -
[webapp] Import Open5e data, and build the two starter packs we are allowed
to publish (#553). The last of the five readers takes an Open5e v2 export in
whichever shape you saved it — a single API page, a plain array of records, or
a folder or zip with one file per resource — and brings in its creatures as
full stat blocks alongside its spells, conditions, rules, items, feats and
character options. An Open5e record carries no licence of its own: the licence
belongs to the document it came from, so the reader looks each one up. The
two SRDs and the Black Flag reference document are CC-BY-4.0, the Kobold Press
books and the other known third-party documents are OGL 1.0a, and a document
Quest Board does not recognise gets no licence at all rather than a guess — it
still imports, and the exporter withholds it. Alongside the reader, two build
scripts underscripts/content/produce the starter packs an administrator
can fetch on request:build_5e_starter.pytakes an Open5e export and keeps
only thesrd-2014andsrd-2024documents, andbuild_pf2e_starter.py
takes a Foundry Pathfinder 2e checkout and keeps only the records the ORC
License covers, dropping creature flavour text because that is where the
Reserved Material the licence excludes lives. Each writes a.qbpackcarrying
the notice its licence requires — the SRD's Creative Commons attribution to
Wizards of the Coast, or the ORC Notice with Paizo Inc. named as the licensor
of the products the pack drew on — and aSOURCES.mdbeside it listing the
documents, the counts, and everything withheld with the reason.
Operators: these packs are published as release assets and fetched on
request, never bundled in the image, so an operator who wants neither ships
neither. Each build is pinned to an upstream commit and is byte-identical when
re-run from the same source, so a content update arrives as a diff someone can
read rather than as an opaque new blob. -
[webapp] Load the rules and bestiary content you already have, and look it
up without leaving Quest Board (#553). Quest Board still ships no game
content of its own — bundling a monster manual would make every operator a
distributor of it. Instead there is now an importer: an administrator loads a
pack for the whole instance, a GM loads one for a single campaign, and either
affirms at the point of import that they are entitled to. Quest Board records
who affirmed what and when, shows the licence notice the pack carries,
searches it locally, and exports back only the records whose licence actually
permits it — the rest are listed in the export with the reason they were
withheld. This first slice reads Quest Board's own format in three shapes: a
.qbpackarchive, a plain JSON array of records, and a CSV you can write in a
spreadsheet (kind,name,level,traits,summary,body). A campaign's own version
of a creature ranks above the instance-wide one of the same name and both are
shown, each saying which pack it came from; a GM's records stay GM-only until
they say otherwise. Operators: an administrator can disable any pack on
the instance — a campaign's included — which removes it from every search
immediately while keeping the rows, and can remove it outright; both actions,
and every import and export, are written to the audit log. Descriptions
imported as HTML are sanitised before they are stored, so nothing executable
survives an import. New migration1c2d3e4f5a6baddscontent_packsand
reference_entries; the backend gains two dependencies,nh3and
markdownify. Full format reference indocs/CONTENT-PACKS.md. The screen
that drives it is the next entry; the player-facing search lands with the
v4.4.0 interface work, and the API is documented indocs/API.md. -
[webapp] A screen for loading content packs, and for the promise you make
when you do (#553). Administrators get a Content packs tab in Admin for
the packs the whole instance can search; a GM gets a Content packs panel
on their campaign page for that campaign's own. Both take a file or a URL,
let you name the game system and the format (or leave it to detect), and say
who imported each pack, when, what is in it, and under what licence — with
the licence's own notice one click away, because that text is attribution the
licence requires be carried. The affirmation you agree to is fetched from the
server and shown word for word rather than copied into the app, the box is
unticked again after every import because affirming is per pack and not per
visit, and if the wording changes while the page is open the server sends the
new text back and the box asks again. A pack too large to import in one
request reports its progress with a clock on it and updates itself when it
finishes; warnings from an import are shown rather than counted. Export is a
download link, and removing a pack asks first and names how many records go
with it. Operators: disabling and re-enabling a pack stay administrator
actions on both scopes — a GM can see that one of their packs was disabled
and why, and can remove it, but putting it back is the host's call, since
disabling is how a rightsholder's notice gets answered.
Fixed
-
[webapp] Suggested wiki updates no longer arrive in duplicate (#609).
Every suggestion for a session was written twice — twelve suggestions meaning
twenty-four rows to read and dismiss — whenever the matching step took longer
than a minute, which with a self-hosted model is every time. The step that
writes the suggestions waits for the earlier phases by asking to be retried,
and that request was being caught and turned into a second retry with the
same delay, so two identical copies of the job woke together and each wrote a
full set. The retry is now passed through untouched, and the write takes a
lock and recognises when its twin has already finished, so a repeated
delivery does nothing at all. Existing duplicates are not cleaned up
automatically: press Look for wiki updates again on an affected session
and its suggestions are replaced with one clean set. -
[webapp] A long session's summary no longer spends its spare room on the
start of the evening, and talk that is not about the game stays out of it
(#603). Since #589 the summary takes one event from every stretch of the
session that has any, and because some stretches are quiet there are usually
a few places left over. Those went to whatever was left in the order it
happened, so always to the start of the evening and never to a busy ending.
They are now shared out in proportion to how much each stretch still has to
offer: a stretch where a lot happened gets more of them, wherever it falls,
and a quiet one keeps its one. A busy stretch's events are chosen from across
it rather than from its first few seconds, and the summary is now always
written from the session's last event as well as its first, so its closing
paragraph is about how the evening actually ended. On the real 54-event
session behind this fix, that moves three of the thirty-six places into the
second half of the evening and brings back both of the late events the old
choice left out. It is a better sample, not a guarantee about any single
event, and every event is still listed in full under the summary. Separately, the summariser was
told to leave out talk that is not about the game and recorded it anyway, as
if it were story — a conversation about another video game arrived as an
event, and a player's pun reached the prose. It now labels that talk as table
admin, the same as the rules calls in #596, so it is kept in the event list,
folded away, and never written into the summary. -
[webapp] A summary no longer ends on a "What's next?" with nothing after
it (#595). Every summary prompt asked the model to "end with a short
'What's next?' hook", and Gemma 4 26B took the quoted phrase as the thing to
write: both reruns of one session finished on a bareWhat's next?line and
stopped, which reads as a summary cut off mid-thought. The prompts now ask for
what that last paragraph should say — one or two sentences on what the party
is left facing or has decided to do — and if a model still ends on the
heading alone, the heading is removed before the summary is saved (and before
any "part of the session could not be summarised" note is placed, so the note
never lands in the wrong spot because of a heading that was about to go). A
hook with something after it, or "what's next" anywhere in the prose, is left
exactly as written. Each run records whether this happened ashook_dropped,
so whether the new wording holds for a given model is visible across runs. -
[webapp] The same event, word for word, no longer appears twice in a
session's events (#588, in part). On one real session "Clio discovers the
water is much deeper…" was listed twice at 00:04:56, and two more events were
doubled the same way, because deduplication only merged copies that cited the
same transcript line for the same person, and these copies had drifted apart
on one or the other. Two events with the same start time and the same words —
ignoring capitals, spacing and punctuation at either end — are now one; the
first is kept, unless only the later copy passed the transcript check. Nothing
cleverer than that: two differently worded descriptions of one moment, or one
moment split into two events, are still left alone, because telling those
apart needs judgement and #588 stays open for it. Each run now records how
many duplicates deduplication removed asbeats_collapsed(migration
8d9e0f1a2b3c, which also addshook_dropped). -
[webapp] The GM running the table is no longer written up as the story
(#596). Replaying a real session, several of the "events" the summariser
confirmed were the GM running the game: fixing a character's position on the
map, moving another to the upper layer, explaining what a thresher is (twice),
asking everyone for their backstories, three on which skill reading a journal
needed, and three on adding a skill to a character sheet. Each one passed the
checks, because the GM really did say it, so they filled up the event list,
took places among the thirty events the summary is written from, and one
reached the prose as "the group received an explanation regarding what a
thresher is". Telling the model to leave such lines out had not worked on
that model, so now it labels them instead, as table admin, and the label
does the rest: table admin is never written into the summary and never counts
against it, and in "Everything that happened" it sits behind one quiet line —
"9 table notes" — that opens them in place, each marked as such. A line
the model labels as table admin by mistake is therefore still on the page,
one click away, not lost. The same goes for the GM answering a question about
something earlier (#584), which is now labelled rather than dropped. Each run
also logs how many events look like table admin but were not labelled that
way, which is how we will know whether a given model is using the label. -
[webapp] A long session's summary no longer skips the start of the session
(#589). The cap added in #568 keeps the summary to thirty events and picked
which thirty by kind, preferring reveals, decisions, fights and travel over
"social" and "other". On the first real session it ran on, that cut the
session's own opening: the party's long rest, waking up somewhere they had not
fallen asleep, and working out that the caverns had shifted were all labelled
"other" by the extractor, so each of them lost its place to a reveal from
forty minutes later, and the summary began instead with one character walking
down a ramp. Fifty of that session's eighty-one confirmed events — 62% of
it — sat in the losing tier, because six labels borrowed from combat and
intrigue do not describe a session spent exploring. So the thirty are now
chosen by when they happened: the session is cut into thirty equal stretches
of time, every stretch that contains anything contributes an event, and the
labels only decide which event comes out of a stretch. A summary chosen this
way can still give a moment less weight than you would have; it can no longer
leave out the part of the evening that set everything else up. Sessions with
fewer than thirty events read exactly as they did before, and every confirmed
event is still listed in full underneath the summary either way. -
[webapp] The summariser no longer takes the GM answering a question as
something that just happened (#584). On one session the party was thrown
clean across the map by shifting caverns, and every summariser tried on
it — a 9B model, a 26B one, a frontier hosted one — put them back in the
place they had been thrown out of. All three were repeating the same
extracted event, and that event had passed every check: the GM really had
said "you're in ", a few turns into the players asking where they had
been before the caves. The words were spoken and the speaker spoke them,
which is all the validator can decide from the text; the tense was answering
a question about the past, which it cannot. So the extractor is now told
that a GM line answering a player's question — about earlier events, the
rules, the map or a name — is context rather than something happening now,
and may only be recorded as an event when the lines around it show the
fiction changing. Nothing is rejected on a guess about tense: a beat whose
only evidence is a short GM reply just after a player's question is counted
in the logs so we can tell whether the rule is working, and still goes into
your summary, because a heuristic sure enough to delete a real event from a
session is not one anybody has measured yet. That counting was itself blind to
start with, and found nothing on the very session it was written for: it was
reading the shortened transcript the summariser is prompted from, in which a
run of lines from one speaker is merged into a single line — so a GM answering
in three short bursts looked like one long line of narration, and "short reply
just after a question" could never match it. It now reads the lines as they
were actually spoken, and the counts mean what they say. -
[webapp] The provider concurrency cap now actually applies to the work that
happens after a session (#573). The cap from #356 exists so that a burst of
finished sessions costs latency rather than failures, and the post-session
fan-out — storyline, title suggestions, lore proposals, highlights — was
running entirely outside it. Every LLM call those tasks made logged "Could not
reach Redis to take a llm slot … (RuntimeError: Event loop is closed); running
without a concurrency cap for this call", and then did exactly that. Redis was
fine; the connection to it was not. A Celery worker runs each task in its own
asyncio.run(...), an async Redis connection belongs to the event loop it was
opened on, and the pool holding it was shared for the life of the process — so
from the second task onwards the worker was asking a closed loop for a slot.
Because the cap fails open by design, nothing looked broken: a self-hosted
llama.cpp simply took more concurrent requests than it declares and slowed all
of them down together, which is the failure #356 was written to prevent.
Connection pools are now kept per event loop, and a closed loop's pool is
dropped — the same answerapp.database.task_sessionalready gives for the
database. The one other async pool shared across the process, behind the bot
event stream, is fixed with it: its two beat tasks would have failed more
quietly still, since the unread-trim check reads any error as "no stream yet"
and reports no data loss. -
[webapp] Session summaries are told the truth about the order events
happened in (#566). Quest Board extracts the events of a session more than
once — a second pass reliably finds things the first missed — and then hands
the merged list to the model that writes your summary, with the instruction
that it is "already in chronological order". It was not: each pass was sorted
on its own and the passes were then stacked one after the other, so the list
read as the whole session followed by the bits found on the second look. Every
summary was affected, because every run takes at least two passes. The list is
now put in time order at the door the compose step cannot go round, so the
order the model is told it has is the order it gets — and the summary follows
the session rather than the order the events happened to be noticed in. -
[webapp] The quote board reads the whole session again, instead of
whatever fitted (#575). Picking the memorable lines out of a session was the
one step that still sent the entire transcript in a single prompt, however
long the session was. On a real one that came to about 31,000 tokens against a
22,900-token budget, and the log said as much before sending it anyway — so a
local server that quietly drops the front of an over-long prompt answered from
the tail of the session, without its instructions, and the quotes came back
thin with nothing to explain why. The transcript is now split into windows
that fit, exactly as the summariser's own reading of it already was, and each
window is asked separately. The number of quotes you asked for is still the
number you get: they are taken a few from each part of the session rather than
all from the first, so a four-hour game no longer produces a quote board made
entirely of its first twenty minutes, and "key moments" are capped for the
session instead of per call. One window failing no longer loses the rest of
the board. Nothing changes for a session that already fitted in one prompt. -
[webapp] A part of the session the summariser could not read is retried,
and then said out loud (#572). Quest Board reads a session for events in
consecutive windows, and a window whose call to the model failed was skipped:
the summary simply did not cover that stretch, and the only trace was a line
in the worker log that ended with the provider's error message — which for a
local llama.cpp router is the empty string, so it readBeat extraction failed for window 4/5 (pass 1):and stopped there. On the session that prompted
this, window 4 of 5 was the payoff, it failed on every pass, and the summary
covered none of it with nothing anywhere to say so. Three changes, all in the
direction of saying what happened: the log now names the exception's type and
its repr, and describes "the model returned nothing" in different words from
"the call failed"; a window that fails is asked once more before it is given
up on, since these failures come and go per window; and if it still returns
nothing on every pass, the summary itself ends with a plain sentence naming
the missing range — "Part of the session (from 00:41 to 00:52) could not be
summarised because the summariser returned nothing for it; the transcript is
complete" — placed before the "What's next?" hook. A quiet window that the
model read and found nothing in is not a failure and says nothing. Which
windows were lost is also recorded on the summarisation run aswindows_failed
(migration7c8d9e0f1a2b), so the run history can be asked how often this
happens on a given endpoint. -
[webapp] The check that watches for invented people stopped reporting
"Fortunately" as one (#567). After writing a summary Quest Board reads it
back and looks for anyone the verified events, the speakers and the characters
cannot account for — a cheap guard against a model adding a person nobody
played. It decided what a name was by capitalisation, so every sentence that
opened with an ordinary word was a person: real runs reported "Early",
"Fortunately", "Undeterred", "Continuing", "Ahead", "Getting", "Whatever",
"Among" and "Rounding". English capitalises the first word of every sentence
whether or not it is a name, so that position is now treated as saying
nothing: an opening word counts only if it is a name Quest Board already
knows, or if the summary uses the same word capitalised mid-sentence, where
grammar does not explain it. A genuinely invented name still gets reported the
moment the prose mentions it anywhere but a sentence's first word — which
composed prose does, because it introduces a person and then talks about them.
This has always been a log line and never changed or withheld a summary; what
changes is that the log line means something again. -
[webapp] Transcripts no longer contain the things Whisper says about
silence (#569). Speech recognition writes text for near-silent audio, and it
writes the same things every time: television caption credits ("Sous-titrage
ST' 501", "Closed Captioning by …", "Red Bee Media"), a bare "Thank you.", a
lone full stop, one word looped a dozen times, a fragment in a language nobody
at the table speaks, and — because Quest Board tells the model your campaign's
proper nouns before it listens (#355) — one of those nouns handed straight back
to us. One real six-speaker session carried 143 of these among 1,778 lines.
They cost tokens in the summariser's context, they were cited as evidence for
events that never happened, and a model that reads "Thank you." seventy-eight
times writes about how polite everybody was. A filter now drops them before
the transcript is assembled, matching whole lines only — "Thank you." goes,
"Thank you, Kira, that was genuinely heroic" stays — and never a name a person
might have said on its own, so "Wilhelmina?" survives while "Office of
Prophetic Fulfillment" does not. What it removed is counted per rule on the
session's summarisation run, so a GM can see that lines were taken out and how
many; the lines themselves are never logged. Migration3e4f5a6b7c8dadds one
nullable column tosummarisation_runs. -
[webapp] Pages served beside the app on the same hostname no longer get
bounced to the dashboard (#535). The installed app's service worker answered
every navigation on its hostname with the app shell, so a page the reverse
proxy serves under a path of its own, such as the design-review feedback site
at/feedback/, loaded as Quest Board and was sent to the dashboard by the
router, in any browser that had visited the app before. The worker now leaves
/feedback/alone, uncached, as it already did for the API and the sign-in
flow. Browsers that hold the old worker pick up the new one on their next
visit and are right after one reload. -
[webapp] Editing a summary and then clicking away no longer loses what you
wrote (#464). Correcting a session's summary or transcript and then heading
off somewhere else — the nav bar, a campaign link, the browser's Back button —
discarded every word of the correction without a murmur. Closing the tab or
reloading had asked first since #410, but a click inside Quest Board never
leaves the page as far as the browser is concerned, so nothing was there to
ask. Leaving a half-written edit now stops and asks whether to discard it, and
staying put leaves the text exactly where it was. Nothing changes if you have
saved, or if you opened the editor and typed nothing. -
[webapp] "Saved" now stays on screen under a reviewed stat block (#182).
Correcting a proposed stat value on a wiki draft autosaves it, and the little
status line beside the stat block was supposed to say so. It did — for a
single frame, because the same code that noticed the form was no longer
unsaved immediately reset the line back to blank. In practice the autosave
looked like it had done nothing, which is the one thing a silent background
save must never look like. The line now reads "Saved" until the next edit.
Internal (no operator-visible change)
-
[repo] The v4.4.0 design record and a self-hosted feedback site (#361,
#362, #535).docs/design/v4.4.0-concepts/holds the brief's sources, the
surface inventory, the wireframe kit and the sixteen concept fragments that
build the checkpoint A review pages.tools/feedback-site/is a small,
separate container (FastAPI, a JSON-file store) that serves the players' and
GMs' review pages with a form and an admin readout, so beta testers without
accounts can answer. It shares nothing with Quest Board and is not deployed
with it. -
[webapp] Backend support for an admin view of every campaign on the
instance (#437).GET /api/admin/campaignsreturns each campaign's
ownership, member/session counts, Discord routing flags and retention
overrides regardless of the caller's memberships — metadata only, no campaign
content. The Admin screen that uses it lands later in the milestone. -
[webapp] Adopted the React Compiler lint rules from
eslint-plugin-react-hooksv7 (#182). The frontend enabled two of the
plugin's rules by hand and deliberately skipped itsrecommendedpreset,
which in v7 also brings the compiler's own checks — purity, refs,
immutability, set-state-in-effect and a dozen more. The preset is now on in
full, and the forty violations it found across twenty-two files are fixed
rather than suppressed: mostly loading and "stale" flags that an effect
raised on its way past, rewritten as values derived during render from what
the data in hand was fetched for. There are noeslint-disablecomments for
any of it. Running the compiler itself over the build remains a separate
decision that this change does not make.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
-