> ## 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.13.1 — the plugins had nothing to run on

0.13.0's headline was that built-in plugins were never started. They are now,
and it turned out to be the top layer of the same fault: **no packaging format
Cordial ships installs or bundles Deno**, and plugins are Deno programs. Not the
deb, the rpm, either AUR package, the AppImage or the Flatpak. So plugins still
could not run anywhere except a machine that happened to have an interpreter
already — which is what an Arch user reported, and they were right.

Twenty-one commits. Also here: the AppImage's web view, which never opened at
all; a flash of the GTK background when `/` opens chat; a consent prompt that
asked again every time Settings was opened; and a FastFlags page with colour and
live errors.

## Plugins had no interpreter, anywhere

`deno` is in Arch's `extra`, and in neither Fedora (`dnf5 list deno` on 44
returns nothing) nor Debian (its own source index answers `"exact": null`). It
went unnoticed here because this development machine has Deno from Homebrew.

**The Flatpak decides the shape of the fix.** Inside one, `PATH` is the
runtime's, and Cordial deliberately takes no route to the host —
`flatpak-spawn --host` needs a D-Bus name
[ADR-018](/adr/ADR-018-plugin-sub-sandboxing) refuses, because it is the same
name that runs an arbitrary command on the host. A Flatpak user therefore cannot install an
interpreter by any means available to them. Cordial has to provide one or
plugins are permanently dead there.

So Settings, Plugins, grows a row **only when there is no interpreter**, with a
Download button. It fetches a pinned Deno — version and SHA-256 both constants
in the source, so nothing asks the network what the current release is — checks
the hash over the stream before the file gets the name anything looks for, and
extracts atomically, because the check that decides whether to download is "is
there a file at this path" and a torn extract would be mistaken for an
interpreter for ever.

This is not a new class of trust: Cordial already downloads a 118 MB
`libroblox.so` and loads it as native code in-process.

Arch never fetches. Its package now depends on `deno` properly, and `PATH` is
searched first. **Both `.SRCINFO` files were stale** and did not list it —
that is the file the AUR actually reads for dependencies, so an AUR install
would not have pulled Deno in even after the `PKGBUILD` was fixed. Corrected
here, along with the missing `bubblewrap` optional dependency.

The end-to-end download was run against Deno's real release and the extracted
binary reported the pinned version. **What is not verified is the Settings row
itself** — this machine could not open a second Settings window while a client
held the single-instance lock.

## The AppImage could not open a web view

A user's AppImage answered a web view with:

```
Unable to spawn a new child process: Failed to spawn child process
"/usr/libexec/webkitgtk-6.0/WebKitNetworkProcess" (No such file or directory)
```

They then installed WebKitGTK on the host and it changed nothing, which is the
detail that identifies the bug. The bundled `libwebkitgtk-6.0.so.4` reaches five
things through absolute paths fixed at *its* build time, and only Fedora and its
derivatives put the helpers in `/usr/libexec` — Debian uses
`/usr/lib/x86_64-linux-gnu/webkitgtk-6.0`, Arch `/usr/lib/webkitgtk-6.0`. So
installing the host package leaves the baked-in path just as empty.

`WEBKIT_EXEC_PATH` was already exported by `AppRun` and **does nothing**: it
appears zero times in the library, against thirty-odd other `WEBKIT_*` names
that do appear. A knob that looks like the fix and is inert is worse than no
knob.

The paths are therefore made to *exist* rather than redirected. `AppRun`
re-execs under `bwrap` with an overlay over `/usr/libexec` — an overlay rather
than a tmpfs so the host's other contents survive — and binds the bundled
helpers in at the name the library asks for. It probes first and falls through
unwrapped, saying so, on a kernel or policy that refuses unprivileged
overlayfs; `CORDIAL_APPIMAGE_NO_WRAP=1` turns it off entirely.

Verified with a control on a host standing in for one that never installed
WebKitGTK: with the wrap, both helper processes ran from inside the image and a
page reached `load-changed FINISHED`; same image, same host,
`CORDIAL_APPIMAGE_NO_WRAP=1`, the reported error verbatim. **What is not
verified:** the tested image's binaries came from an installed rpm rather than a
fresh build, the probe was a bare `WebKitWebView` rather than Cordial's own
sign-in view, and nothing but Fedora was tried.

## The `/` flash, and the consent prompt that would not stay answered

Two user reports, one cause each.

**The GTK background flashed when `/` opened chat, but only while a movement key
was held.** The restacking code already had the right idea — make the background
transparent, repaint, *then* lower the subsurface — and relied on a `repaint_now`
that was `queue_draw(); pump();`. The pump runs at most 32 non-blocking
iterations, deliberately, because the engine's pump must return. Idle, 32
iterations reach a frame easily. Holding a movement key fills the context with
key repeats, the 32 are spent on events, and the frame clock never ticks inside
them — so the restack went out under a parent that was still opaque. That is
exactly why it needed a key held to reproduce, which is the part of the report
that identified the mechanism. It now waits for `after-paint` on the frame
clock, bounded by 40 ms, because the thing being waited for is a frame and
frames are a duration.

**The consent prompt returned every time Settings was opened**, even after the
plugins were enabled. It was recorded in the response handler, which fires only
if that dialog is answered — and every built-in plugin row presents its own
prompt as the page renders, so several stack at once. Answer the top one, close
Settings, and the rest are torn down with their parent without a response;
nothing is written and all of them ask again. "Has this profile been asked" is
now true the moment the question is on screen.
[ADR-003](/adr/ADR-003-plugin-isolation)'s default deny is untouched:
grants are still only written by the allow branch, so a dismissed prompt grants
nothing and merely stops nagging.

**Neither is verified by observation.** The flash is GTK's own buffer showing
through, which `cordial_screenshot` cannot see — it reads the engine's swapchain
— so confirming it needs a compositor capture at frame rate. The mechanisms are
read from the code and the reports; the fixes follow from them. Worth a look
before either is called done.

## FastFlags: colour, and errors as you type

A comparison with Pigment, another Linux Roblox launcher, made two gaps obvious.
Its editor is syntax-coloured; ours was a wall of monospace where every flag
name looked like every value.

GtkSourceView would have added a runtime dependency to five packaging files to
colour one text box, so the document is scanned here instead and painted with
tags — two palettes, repainted when the system theme flips, because a text tag
holds a literal colour and does not follow a theme the way a CSS class would.

**Applying was previously the only way to find out whether a paste parsed**, so
the feedback for a misplaced comma was a toast after a save that did not happen.
The status line now names the error as you type and Apply goes insensitive while
the document does not parse. Nothing was ever written in that state anyway; the
only change is whether pressing the button is how you find out.

The actions moved, for the third time, into a row beneath the editor with the
file path. The first two arrangements were each half right, and the lesson from
the second was the wrong one: the problem was that nothing was grouped, not
where the group sat.

**The scanner is tested; the colours are not.** No nested compositor was
available to photograph the rendering.

## Discord presence

**A game's cover art, not Cordial's icon.** A game that set `largeImage`
through BloxstrapRPC got Cordial's logo, because the picture was thrown away on
the way to the payload by a comment that contradicted the one a screen above it.

**And a presence for games that send no RPC at all**, field for field the shape
Bloxstrap uses, read from its source (MIT) rather than guessed: the experience
name, `by <creator>`, the game's icon as the large image, the Roblox badge as
the small one, and the join time as the start. Every default goes in only where
the game said nothing, so a game that speaks still wins.

**One correction, and it is the important one.** The previous attempt at this
shipped a URL that returns **404**, and it was reported here as working because
Discord *accepted* it — Discord rewrites any URL to its proxy form without
fetching it, so acceptance measures nothing. The result on a real profile was a
broken-image placeholder where Cordial's own icon used to be, which is worse
than the bug it replaced. The check that settles it is `curl` against the URL,
and it takes one command. The working endpoint is `thumbnails.roblox.com`, and
Cordial now resolves pictures through it, cached, off the log-watching thread.

**A plugin never handles a URL.** Cordial issues an opaque key per picture and
the payload echoes one back; an unresolved key renders as no image. That was the
alternative to host-checking a URL and it is the stronger one: checking hosts
still lets a URL cross the boundary and then argues about which ones are
acceptable, whereas a key cannot be a link at all.

**The small image will not appear yet.** Bloxstrap's small image key is the
literal string `roblox`, a Discord *application asset* rather than a URL, which
renders only once an image of that name is uploaded to the application's Rich
Presence assets. Only the application's owner can do that. Until then Discord
draws no badge and nothing else changes.

**Games are now told this launcher speaks BloxstrapRPC.**
`FFlagUserLaunchedWithBloxstrap` is how an experience finds out whether the
launcher under it implements the protocol, and a game that gates on it says
nothing at all. Bloxstrap's own RPC test place reports the answer on a board,
and against Cordial the board said NO. It is not an engine flag — the name
appears nowhere in `libroblox.so`; the launcher injects it and the game asks
about it. A user's own FastFlags still overrule it.

**And the log tells the truth now.** The broker wrote the frame, read Discord's
reply and threw it away. Discord refuses an activity it will not store with an
equally well-formed frame carrying `evt: "ERROR"`, so a *refused* presence was
returned to the plugin as `ok`. **Every "presence.set came back: ok" in a log
from 0.13.0 or earlier means "Discord answered", not "Discord accepted"**, and
anything concluded from those lines needs re-reading with that in mind.

## Launch stopped scanning 118 MB of engine

Profiled with `perf` before porting anything from another project, and the
largest named cost in Cordial's own code was ours alone: reading the engine's
version, so the client can tell the server which build it is, scanned the whole
of `libroblox.so` byte by byte at every launch. It is now cached against the
file's length and mtime, so a launch reads a stamp instead.

## The TaskScheduler crash: three more reports, and a diagnosis

Two more users hit `Can't initialize the TaskScheduler before flags have been
loaded` — Linux Mint and Arch, on top of CachyOS twice. **That is three
distributions across both the AppImage and the Flatpak, so the distribution is
not the variable and neither is the packaging format.**

The Mint log is the first with the whole startup in it, and it names something
two sessions of inference could not: on that machine the engine build does not
export `nativeInitClientSettings` under the name Cordial looks for, so **the
flags were never delivered at all** and the engine crashed on exactly what it
said. The two-physical-core theory that looked so promising was tested with
`taskset` on the same binary and profile and did not reproduce.

Cordial now **says so when a native is missing** instead of skipping it in
silence, and the report names its own answer. That is a diagnostic, not a fix:
the engine version there is 2.734.0.917 against 2.736.0.1408 here, and a JNI
method renamed or moved between builds cannot be tested from this machine. If
you hit this, the log now carries the evidence.

## Measured and not changed

Three things were investigated at length and produced no code, which is worth
saying so nobody reopens them from a title.

**The startup freeze holds nothing.** Three frozen clients were caught under gdb
on a signed-in profile. The engine thread is **parked, not spinning** — its poll
count moves 199 in ten seconds at 1.6% of a core, which is the ceiling the idle
backoff produces — and **not one of 73 threads is on a mutex.** It is waiting on
an in-process pipe nobody writes to. That refutes two claims in the
investigation notes, which have been corrected.

**No performance work was worth taking from mocktail.** The lock traffic that a
lock-free JNI scheme would have targeted is 4.7% of startup, and LBR call stacks
put 177 of 179 caller frames inside `libroblox.so` — the engine locking against
itself, with nothing on our side to make lock-free. Every other candidate died
the same way, on measurement.

**All three clients were re-measured side by side**, same method, same host, one
at a time, three passes each — because the standing comparison was a month old
on our side and older on everyone else's:

```
                 peak CPU          idle CPU      RSS
Cordial          203 210 123  179    5.5%      500 MB
mocktail         148 169 146  154    8.1%      997 MB
Sober            183 202 214  200    7.7%     1216 MB
```

Cordial uses **half the memory of either**, and barely moves — 10 MB of spread
where mocktail swings 128. Peak startup CPU is no longer the outlier it was at
0.6.0 (342% against 142% and 153%). Three ways this is not like-for-like, all
favouring Cordial and none corrected for: Cordial ran signed out on a fresh
profile while the controls ran their existing signed-in installs; Cordial was
one process against twelve and thirteen; and Cordial was a local build against
two Flatpaks.

## What is still broken

Everything 0.13.0 listed, unchanged: **the startup freeze when signed in** —
better characterised now, not fixed — the black canvas inside an experience,
text in a box disappearing when you click off it, the editor lagging a text box
that Roblox is animating, and the web-view dialog's clicks and cursor.

The TaskScheduler crash above is **not fixed**, only diagnosed and instrumented.

`gamepadType`'s ordinals are still unestablished. Settling it needs a controller
and a game.

**Three of this release's fixes have not been seen working**, each for a stated
reason: the `/` flash and the consent prompt (no compositor capture, no second
Settings window), the FastFlags colouring (no nested compositor), and the
plugin-download row in Settings. The mechanisms are established and the
components underneath them are tested. That is not the same as having watched
them work, and this note will be corrected rather than left wrong if any of them
turns out not to.


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