The whole v1.9.0 cycle ran without a version bump, so the plugin still reported
1.8.8 in-game. For a tester beta that is untenable: bug reports would name a
version that does not identify the code they ran.
Download links stay pinned to v1.5.6 on purpose. There is no v1.9.0 release, and
pointing at an artifact that does not exist is worse than the visible mismatch
between assembly version and download target. Public stays on v1.5.6 until
v2.0.0.
TabsListLock never covered ChatColours, PrivacyPersistChannels or
RetentionPerChannelDays, yet the settings UI mutates them from the draw thread
while the retention thread can be serializing the same config. Adding a new key
to a dictionary or a new element to a set invalidates a running enumeration, so
this could throw from inside JsonConvert.SerializeObject.
Not a corner case: the colour picker lists 66 channels but only 25 ship with a
default, so the first edit of any of the remaining ones inserts a new key -- and
the reset button removes a key, which makes the next edit a fresh insert again.
The readers matter as much as the writers. IsAllowedForStorage runs per message
on the worker thread and asks PrivacyPersistChannels whether a channel may be
stored; a Contains racing an Add that resizes buckets can answer wrong, and that
answer decides whether a message is written to disk. The retention sweep
enumerates RetentionPerChannelDays on the framework thread while the wizard can
clear it -- Clear does not throw there, it just cuts the enumeration short, so
the sweep would run on half a policy.
New ConfigMapsLock covers all of it. It sits inside TabsListLock (that edge is
real, AutoTellTabsService calls SaveConfig while holding the tabs lock), never
the other way round -- so every call site closes the lock before saving.
Tab.Clone silently dropped Icon and ChatCodes, both serialized. A reflection
test now walks the serialized fields so a future one cannot slip past.
Also: CurrentTab read Count and [0] as two separate accesses.
Sidebar, top tabs and status bar each read Config.Tabs on their own, unlocked,
while the worker added or evicted tabs. That gave three independent views of a
moving list: an index built in one place could resolve to a different tab a few
lines later, which showed up either as an out-of-range crash on the draw thread
or -- worse, because it is silent -- as a click landing in someone else's tell.
MainWindow now takes one snapshot under the lock and passes it through the whole
frame. Deliberately a shallow copy: tab identity is compared by reference all
over the draw path, so cloning would break every ReferenceEquals and Contains.
ChangeTabDelta and ResetActiveTabIfRemoved run on the framework thread and keep
their own locked reads instead; ThemeQuickPicker locks its own copy, since
reaching it would mean threading a parameter through InputBar, which popouts
share and which has no tab list.
Widget IDs move from list position to tab.Identifier. ImGui carries popup and
widget state across frames under that ID, so a position-based one re-binds an
open context menu to a different tab as soon as the list shifts -- a snapshot
cannot fix that, it spans frames. This also resolves top tabs visually merging
into each other when the list changed.
The sidebar section headers counted over the live list while the rows came from
BuildRenderOrder, which skips popped-out tabs. Both sides take the same
predicate now, so the count matches what is drawn.
ProcessMessage walked Config.Tabs live on the worker thread while SaveConfig's
strip and the auto-tell spawn mutated the same list under TabsListLock. The
resulting "collection was modified" was caught by the pending-message handler
and only logged -- so the message was dropped entirely: no tab entry, no sound,
and MessageProcessed never fired, which also meant no tell tab and no routing.
Silent message loss, exactly under the load where it hurts.
The loop now runs over a snapshot taken under the lock. AddMessage stays
outside it, so the lock order (list outer, MessageList inner) is unchanged.
SelectNotificationSound reports which tab it picked, so playback can skip a tab
that disappeared between snapshot and sound -- otherwise the snapshot would let
an evicted tab still make noise.
While here: the current tab was read twice despite the comment claiming it was
snapshotted once.
GetTellHistoryWithSender filters on Receiver and orders by Date DESC. Without a
matching index SQLite sorted the whole receiver history into a temp b-tree
before yielding row one -- measured 10 ms to first row against 9621 tells, all
of it under TabsListLock, which defeats the early break in the caller.
(Receiver, ChatType, Date) does not help: the ChatType IN filter sits between
the equality prefix and the sort column, so the temp b-tree stays. Verified on
a real database: plan now reads SEARCH ... USING INDEX idx_messages_receiver_date
with no sort step.
No SQL LIMIT -- an earlier 500-row cap was removed in v1.4.10 because it cut
less-frequent partners off the back of the window, and 83% of partners have
fewer than 21 tells in total.
The migration dispatcher is cumulative, so Migrate5 is appended to every
existing case, not just the new one.
ImGui sliders report a change in every frame the value moves, so dragging one
rewrote the full 31 KB config to disk per frame -- serialize, fsync and rename,
synchronously on the draw thread. Measured on Linux/Wine that showed up as a
114 ms frame while the plugin itself only drew for 2.9 ms; the rest was waiting
on the write.
The five shared slider helpers now defer SaveConfig to IsItemDeactivatedAfterEdit,
matching what ChatColourPicker already did for the colour wheel.
Renaming a tab needed its own path: the input lives inside a popup, and ImGui
never re-submits it when the popup is dismissed by clicking outside, so
IsItemDeactivatedAfterEdit would not fire and the new name would be lost. A
pending-rename marker scoped to the owning tab flushes it when the popup is
gone -- scoped, because every other tab's Draw reaches that branch too.
DeferredSaveFrames is removed: the debounce was fully wired but never armed,
and this approach makes it redundant.
The three sidebar/coupling steps add, insert and remove tabs straight from
the framework thread while the message worker mutates the same list under
TabsListLock. CurrentTabCouplingStep's Insert(0, ...) is the worst of them:
it shifts every index, so DropOldestTempTab can remove the wrong tab between
its index lookup and RemoveAt.
Locks sit around the individual mutations, never around a Draw call, so no
step holds the lock across rendering.
Rueckbau des Inline-Workarounds. Der Runner konnte den reusable workflow
nicht mehr laden, weil git fetch gegen die Forge crashte. Ursache war kein
Bug, sondern eine Kompromittierung ueber CVE-2026-59774: ein injizierter
packObjectsHook ersetzte git pack-objects durch einen Malware-Dropper.
Gitea laeuft jetzt auf 1.27.2, der Hook ist entfernt, fetch funktioniert
wieder. Die Scan-Definition liegt damit wieder an einer Stelle statt in
sieben Dateien. Details: Obsidian "Projekte/Hellion Forge/Forge
Security-Incident 2026-08-15.md".
act_runner laedt reusable workflows per git-clone ueber HTTPS. Dieser Pfad
ist auf der Forge seit 2026-08-12 defekt (git upload-pack --stateless-rpc
bricht mit BUG "packfile_uris requires sideband-all" ab). Die Scans liefen
bisher nur ueber eine im Juni gecachte Kopie im Runner.
Der Scan steht jetzt vollstaendig in dieser Datei, der Quellstand kommt als
tar-Archiv statt ueber actions/checkout. Zurueckbauen, sobald git fetch
ueber HTTPS wieder funktioniert.
- gate keybind pill-sync on IsChannelOrExistingLinkshell so an empty
linkshell slot no longer desyncs the pill from the real send channel
- close manually-popped pop-out windows on logout via an IsOpen filter
instead of the PopOut flag (which manual pops never set)
- read the router's tell-tab lookup through a lock-wrapped accessor so the
framework thread cannot enumerate Config.Tabs mid worker-thread mutation
- add a "switch on every tell" toggle (default on) and make the auto-open
mode pick the matching layout, so Sidebar vs Top-tab are distinct
- comment corrections (stale/contradictory text, TEST-MIRROR path depth)
The eye/hide button now hides the HellionChat window (runtime-only, via a new
DrawConditions gate) instead of toggling native-chat suppression, matching 1.5.6.
The chat-activation keybind (Enter / "/"), whose dispatch was a dead stub in the
KeybindManager since the v1.6.0 rewrite, is re-wired to MainWindow.ActivateChat:
it un-hides, opens if closed, brings the window to front and focuses the input --
so the chat reacts to Enter again from any state. /hellion is a reliable one-press
recovery (Toggle now clears the hide), and the window always shows on login
(start state no longer read from the persisted flag). Adds HideRestoreSelfTestStep.
Re-wires four 1.5.6 settings that survived the v1.6.0 rewrite as dormant config
fields but lost their UI + consumers:
- ShowTitleBar / ShowPopOutTitleBar: gate ImGuiWindowFlags.NoTitleBar on the main
window (ResolveFlags) and pop-out windows (new PreDraw). Inverted logic matches
1.5.6 (flag set only when the toggle is off).
- ShowHideButton: gate the input-bar hide button on the toggle.
- Use24HourClock: add the toggle (MessageList already consumes the field).
New 'Window style' section in WindowTab; Use24HourClock in ChatTab display modes.
MainWindowFlagsStep extended with the NoTitleBar fresh-base contract.
Brings back the 1.5.4 quick-picker lost in the v1.6.0 rewrite: a palette button
in the input-bar button row (left of the cog) opens a popup that switches the
theme (built-in + custom, active row checked) and jumps between chat tabs without
opening settings. Theme switch mirrors the settings ThemePicker; the tab jump
routes through a new MainWindow.ActivateTab that replays the click path
(previous -> set -> OnTabActivated) so tell/unread handling is unchanged. Main
window only -- pop-out InputBars get a null picker. Adds QuickPickerSelfTestStep.
Brings back the 1.5.6 per-channel colour editor lost in the v1.6.0 rewrite:
presets (incl. brand styling), per-ChatType reset/import-from-game/colour-edit
over Config.ChatColours, the colour-selected-input-channel toggle, and the
theme adopt-banner. The colour-edit drag recolours live but defers SaveConfig
to release (IsItemDeactivatedAfterEdit) to avoid a per-frame full-config write.