> ## Documentation Index
> Fetch the complete documentation index at: https://cordial.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Cordial 0.12.0 — the client says why it failed

A first-launch crash on CachyOS turned out to be two different bugs wearing the
same error message, and neither could be diagnosed from what Cordial printed.
Most of this release is the client learning to say what happened. Text entry
also stopped drawing at the wrong size and stopped needing two presses of `/`.

## The settings document now says where it came from

The engine traps with `Can't initialize the TaskScheduler before flags have been
loaded` when it is handed an empty client-settings document. Cordial printed
nothing about that document at all, so the crash looked like a race and was
diagnosed as one — wrongly, for one of the two reporters.

The default launch path now prints the size and the source:

```
  client settings: 1284288 bytes (cache)
```

and on a failure says which way it failed: `fetched`, `stale cache, fetch
failed`, or `nothing:` with the reason. An unreadable path given with
`--client-settings` now says so instead of silently falling through to the
network, which previously looked exactly like a healthy launch with nothing
behind it.

Measured: a run forced down the empty-document path reproduces the reported
crash exactly, printing `client settings: 0 bytes` before
`nativeInitClientSettings -> 1`; three control runs in the same session each
printed `1284288 bytes (cache)` and did not crash.

## The settings fetch had no timeout at all

It was a bare `ureq::get(URL).call()` with no connect and no read deadline, so a
CDN that accepted a connection and then said nothing would hang the launch
indefinitely. It now uses the same timeouts as the updater: 10 seconds to
connect, 20 seconds in total.

## Focused text was drawn too big

The editor drew text on a focused TextBox noticeably larger than the engine drew
it unfocused, so a box visibly jumped as you clicked into it. Roblox's font
mapping carries a `fromRbxFontRatio` per font id, and the editor ignored it.

Measured on font id 46, Builder Sans, ratio 0.7936507937: the capital "S" ink
height was 11px as the engine drew it, 15px in the editor before, and 12px
after. Thirteen of thirteen boxes checked.

Two font ids can share one file with different ratios, so the table stamps each
id's own row rather than the file's.

## "/" opens chat on one press

Pressing `/` needed two presses to open the chat box. The guard that stops a
focused TextBox eating game input was suppressing the key's *release* as well:
by the time the release arrived the chat box had taken focus, so the engine went
on believing `/` was still held.

Releases are now paired to their presses — a release whose press was forwarded
is forwarded too. A press suppressed because a box already had focus is never
recorded, so its release is still suppressed, which is what keeps typing `/`
into an open chat box from reopening it.

**Not yet confirmed against a live client.** The pairing logic has unit tests
and the reasoning is written out at the change, but nobody has driven a real
`/` at a real chat box and watched it. If one press still does not open chat,
or if a stray `/` now appears in the box, that is this change.

## A sandboxed plugin dies with the client

A plugin's sandbox outlived the client that started it. Killing it now kills the
whole process group rather than one pid, and the sandbox is started with
`--unshare-all`, `--die-with-parent` and `--new-session`.

Measured under an injected panic: seven orphaned sandboxes before, none after.

**The signal case is not covered by that measurement**, and is measured in
0.12.1 -- see those notes. `Drop` does not run when the client dies by signal.

## Discord Presence uses Cordial's real application id

It was using a placeholder, so the presence that appeared was not Cordial's.
There is also now a way to change it.

## X11 pointer locking survived the move to Wayland

Pointer locking on X11 was reconciled against current main rather than lost,
and X11 input now comes from XInput2 with the pointer warp as the fallback.
ADR-028 records what the acceleration row does when XI2 is not available.

## The update path

Eleven separate faults, starting with the one that ate installs. Both archives
now swap as a set or the install is left exactly as it was, rather than being
replaced one file at a time.

## What is still broken

**The startup freeze, when signed in.** Not fixed, and now better characterised:
two new specimens this cycle, one arriving *after* the home page rather than
before it, and one where the settings menu lost a row immediately before
freezing. Reopening usually works.

**A black canvas inside an experience.** Joining works; what you see once there
may not.

**The second half of the CachyOS crash.** Two people hit the same error string
for different reasons. The reporter whose settings document was *rejected* is
now diagnosable. The reporter whose document was accepted, whose flags carry
real values and who crashes later, is not fixed and not yet explained.

**The web-view dialog cursor is unverified, and the code says so.** Whether the
cursor reaches a verification or purchase dialog — and whether clicks leak
through to the game underneath — has still never been observed live. One report
says purchase popups now work; the code that would make them work carries an
`INFERRED` comment saying it has not been run against a live dialog. Both remain
true.

**Empty documents still crash.** Deliberately. With no flags loaded the engine's
assertion is telling the truth, and starting anyway would be a stub that lies —
failing later somewhere with no relationship to the cause. What changed is that
you can now see why in one line.

## Nothing that was proposed and measured away

**A TaskScheduler gate.** The first diagnosis of the CachyOS crash was a startup
race, and the fix was to make the client wait for flags before bringing up the
TaskScheduler. For the reporter whose document was rejected there is nothing to
wait for, and the gate — whose value is captured once and never re-read — would
have been permanently shut, turning a crash into a hang. Dropped.

**Flushing stdout on the crash path.** The theory was that a shorter crash
transcript meant Rust discarded unflushed output. Rust's `io::Stdout` is a
`LineWriter` unconditionally and flushes on every newline regardless of the
destination; 200,000 lines followed by an external `SIGKILL` lost none of them,
and the same crash captured to a file and through a pty came out identical. The
transcripts differ because the two reporters crash at different points. No flush
code was added.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.