Both were live in the running shell and invisible to every check that ran
before it. Found by reading the shell's log after Casey applied.
ThinkingCard: missing `import qs.modules.common.functions`
ReferenceError: ColorUtils is not defined, on every render of every
reasoning segment. My own defect, shipped through a lint that said clean.
The lint was blind because I staged an approximation of the composed tree by
hand instead of linting the composed tree itself. Against the real composed
tree qmllint reports it plainly: 1 ColorUtils diagnostic before, 0 after.
Lint the composed tree. A hand-built stand-in tests the stand-in.
Config: sidebar.ai.fontSize restored
MessageTextBlock (vendor) reads Config.options.sidebar.ai.fontSize and got
undefined, assigning undefined to font.pixelSize on every text render.
Pre-existing, not from 0008 — the vendor delegate used MessageTextBlock too.
Cause is structural: modules/common/Config.qml SHADOWS ii-base's Config
wholesale, so every key ii-base adds after the fork silently disappears. The
key even carries an ii-base comment explaining it was added deliberately as
a live theme binding. Nothing failed; a number was just quietly absent.
Restored as a live binding, never a literal and never null
(null-in-a-serialized-config, 2026-08-11).
Verified: shell alive 12s after the Config edit (the crash-loop window),
config.json 46 keys and zero nulls, both fault lines gone from the log.
Null-valued options serialised into config.json and segfaulted
JsonAdapter on the next launch, killing the shell after load.
Tristate strings (auto/on/off) with boolean fallback.
ii-base and ii-phone each carried a 357-line BarContent to express eight
differences, all of them either "is this widget shown" or "which slot is
it in". No behaviour differed. Every bar edit had to be made twice or
diverge silently against the pin.
Widgets are now declared once as Components in a registry; each slot is a
Repeater over a list of widget names, so placement and order are data.
Config names a slot or a device profile picks it. "auto" derives the
profile from the same cramped-ness test behind useShortenedForm, so the
phone keeps its arrangement with no config file. Loaders are active only
when placed, so an unplaced widget is never constructed.
The agent island becomes a placeable widget rather than a mount smuggled
into UtilButtons: it reaches every device bar from one declaration.
ClockWidget is unified for the same reason. Its phone fork existed solely
to add customFormat, which BarContent now sets on every device.
bar.agentSessions was landing under background.widgets, not bar, so
Config.options.bar.agentSessions was always undefined and only the ?? true
fallback made the island appear wired. Moved, and merged into the existing
bar.indicators rather than declaring a second one.
44px buttons with 35px icons read small on a 540px panel, and a stack's
members were 12px inside a 35px box — unreadable, which is the whole job of
the collapsed form. 56/44 now, and the member icons give back the border and
padding that were being subtracted from them as well as the grid spacing.
Both numbers live in Config so this is a setting rather than a rebuild, with
spin boxes in Settings > Dock. Two numbers and not one ratio: the button is
the row's height and the icon is what you see, and fixing the ratio would
mean either cramped icons in a tall row or icons overflowing a short one.
FileView loads async, so text() was still empty when onReadyChanged read it
once to decide whether this is a new session. It fell through to the unset
env var and persisted "" — the same bug, reached by a different route.
Verified on device: the stored signature now matches viewtop.instance.
HYPRLAND_INSTANCE_SIGNATURE is unset under viewtop, so this compared "" to
"" and isNewHyprlandInstance was false on every start. It gates
lock.launchOnStartup, which has therefore never fired once under viewtop,
and both Idle.qml copies read it as well.
viewtop publishes viewtop.instance beside its control socket — pid plus
startup nanos, different every start. Read that first, fall back to the env
var so a Hyprland session (the laptop is still one) keeps working, and
treat "neither" as the session continuing, which re-locks rather than
assuming a fresh boot.
Two things that worked under Hyprland and stopped when viewtop took the
session, both for the same reason: the binding lived in hyprland.lua and
the daemon was never in the path.
dt2w: note_input recorded the wake and returned Restore, which is
brightness. So a double tap reported to the machine and the screen stayed
dark. It now returns Unblank first — the brightness a panel comes back at
means nothing until the panel is back. Only for the deliberate wakes
(DoubleTapToWake, Squeeze) and only when the panel is actually dark. The
power button is excluded on purpose: apply_gesture already wakes on its
resolved tap, and emitting a second unblank here is precisely how the
wake loop happened in the compositor this afternoon.
Volume: Action::Volume through the executor table, per §12 — a small
daemon reading a signal and calling wpctl is the eighth blind actor. It
fires on the DOWN edge rather than from a recognised gesture, because
BUTTON_MULTI_TAP_WINDOW is 300ms and a volume key that lags a third of a
second behind the press feels broken. The recogniser still sees the
edges, so a future binding table gets volume hold-to-ramp without this
changing.
Shell: HyprlandData did a bare JSON.parse on hyprctl output, which throws
on every refresh when there is no hyprctl — six exceptions a pass, and
every consumer of monitorData.scale got undefined. That is why the region
selector rendered as a sliver: the geometry was not wrong, it was NaN.
Absence is now a latched state, and RegionSelection/OverviewWidget fall
back to screen.devicePixelRatio. Hyprland stays preferred where it exists.
105 tests.
LockedHint was never set on this device, so sessiond's `locked` (which
comes from logind per doctrine §4) was permanently false, request_blank()
timed out LOCK_ACK_BUDGET on every blank, and the panel went dark on a
session nobody could confirm was locked. LOCK-DPMS-LESSONS §1 held in the
code and not on the glass — every blank recorded blank-without-lock.
Three faults, measured 2026-08-02:
- Session.qml wrote the hint through /session/auto, which is the caller's
own session. The shell is not in the session that owns the seat:
viewtop in logind 66 (seat0/tty1), qs in 70. Written to one session,
read from another.
- lockhint.rs resolved via User.Display, which an ssh login is enough to
point at a seatless remote session — and which then went stale when
that session ended. Both sides now ask seat0 for its active session.
- The report was dropped outright. hasLoginctl starts false and only
flips when a Process probe returns; secure=true landed 30s earlier, so
the one edge that mattered hit the guard and was never retried, because
the shell locks once at boot and never changes again. Fourth
edge-vs-level bug after locked_ack, ChargeRate and bootBloomActive.
Verified on hardware: LockedHint now goes yes ~2s after a power tap.
sessiond admits shell_ready from the lease holder's own pid; the shell
carries its lock request through the reload so WlSessionLock adopts the
existing lock instead of taking the unlock branch on it.
Deliberate, never automatic. The autonomous gate it replaces recycled the
tunnel 652 times in 90 minutes and could not be switched off, because
'nmcli connection down' fired the dispatcher that brought it back up.
Reads NM for state rather than tracking a bool. No privilege needed: polkit
already lets the seat user activate a system connection. Registered in the
ii-phone chooser as well as ii-base, or the phone overlay wins and the
delegate never appears.
Verified on blueline that Hyprland advertises primary-selection plus both
data-control managers, so wl-paste --primary --watch sees every selection with
no per-app hooks. Settles TASK-18's selection-detection question as
compositor-level; viewtop is not needed.
The protocol gives no selection rectangle, so anchor is the pointer position at
selection time. Opt-in by default: the watcher observes every highlight on the
device, including passwords, so it is killed while locked and selection text is
never persisted, logged, or trailed.
bootBloomActive defaults true on every scene construction and bootDismissed
resets with it, but the only thing that cleared them was the secure EDGE. A
scene reload during an already-secure lock never moves `secure`, so nothing
cleared the bloom and the phone sat under a full-screen white overlay until the
shell was restarted. Casey hit it and reported it as "the hyprland error
screen"; `grim` returned a solid white 9KB frame, and hyprctl configerrors was
empty the whole time.
Third instance of this exact shape: locked_ack after a sessiond restart, the
ChargeRate stale-scene reload, and now this. Check the level at construction as
well as the edge.
Deliberately not a timeout — a bloom outliving its reason is a bug to locate.
Session.qml and Lock.qml both registered target 'session'; quickshell keeps
whichever loads first and drops the other. Lock.qml's handler is a strict
superset, so the duplicate goes.
The fullscreen overlay sat above squeekboard's layer and owned the whole
screen's input region, so taps in the keyboard area hit the dialog. It now
yields the bottom third while the OSK is up. Exclusive keyboard focus was
tried and reverted — it stopped the keyboard taking touch entirely.
The repo qmldirs listed only repo-owned types; composed dirs also carry
ii-borrowed types (ReloadPopup, PanelLoader, Bar, ...) which a qmldir
present in the dir must list or they stop resolving — broke the phone
shell on deploy. Regenerated every deployed qmldir from the composed
config on-device (capitalized .qml -> type line, pragma Singleton ->
singleton line) and pulled back. Shell verified active, zero load
errors.
19 dirs had .qml but no qmldir -> quickshell config load failed
(File not found @ shell.qml[-1:-1]). Corrected singleton decls in
3 existing qmldir. SystemGestureRail: pill->WlrLayer.Top, oskLift
200, drop oskOpen hide.
speech.stt/speech.tts land in Config so the settings page, the
souveraine-stt CLI, and the keyboard mic key all read one source of
truth. STT defaults to the VPN faster-whisper bridge; the page probes
/health so configured and answering are visibly different states. TTS
is a field waiting for a server.
The session-lock surface was transparent with nothing painting behind it.
TouchLockSurface now owns its backdrop: lock.wallpaperPath when pinned,
else the system wallpaper, else a dark field — plus a scrim for glance
legibility. Glance clock gains lock.twelveHourClock (default on, h:mm ap).
LockConfig grows the wallpaper picker (same FolderListModel grid as the
wallpaper page, no matugen side effects) and the 12-hour switch.
loginctl manages sessions/users/seats only — `loginctl poweroff` exits 1
"Unknown command verb". Preferring it silently broke poweroff/reboot/
suspend/hibernate from every shell surface. Use systemctl; fall back to
loginctl only on elogind systems where it does carry power verbs. The
logind D-Bus capability probe (CanPowerOff etc.) is unchanged — logind
owns policy, systemctl carries out the action.
The C splash now plays only Acts I-III and hands off at ~6.8s; a new
BootBloom layershell overlay (modules/souveraine/boot) resumes Acts IV-V
(Souvie + bloom flower) over the live compositor, clock-aligned to the boot
epoch so the animation is continuous across the process swap. It maps as
Hyprland's first surface (top-level in ShellRoot, before Config.ready),
pokes splash-signal on its first frame to release the GPU, and LockScreen
clears bootBloomActive on secure to fade it out and reveal the lock. Bloom
shader is qsb-compiled from BootBloom.frag at deploy time.
Drop services/Audio.qml from the manifest: it was a pactl mirror written
for native PulseAudio and only tracked the default sink/source. The phone
runs PipeWire now, so ii's native Pipewire Audio service (full graph,
WirePlumber-owned volume) takes over.
Deleted from repo in a prior session while reverting an experiment;
never should have removed a file that wasn't reverted alongside it.
Pulled back from the phone's live ~/.config/quickshell deploy, which
was still running the correct version the whole time.
seat0-active polkit rule so power off/reboot/suspend stop returning
challenge into a session with no polkit agent on the lock surface.
Lock-surface buttons stay gated behind allowPowerFromLock (opt-in) and
requirePasswordToPower. SystemGestureRail progressive gesture + mission
control nav flag.
ext-session-lock finished (another client held the lock) drops
WlSessionLock.locked C++-side; the request bool never heard about it.
Stale true lied to redaction/capability gates and blocked re-locking
(the binding only fires on a false->true edge).
Session.lock() notified logind unconditionally; hypridle's lock_cmd fires
on logind's Lock signal, so every lock echoed back through logind ->
hypridle -> lock() forever. Observed: ~450 lock requests/min for 7 minutes,
re-locking the screen seconds after each unlock. Notify only on the
unlocked->locked edge.
sessiond gave up waiting for the compositor lock ack after one dispatch —
at boot Hyprland defers the ack while the splash holds DRM, so the initial
lock silently failed (journal: 'never acknowledged', 0.7s in) and boot ran
on the legacy lock path. Wait up to 60s for locked/finished.
ext-session-lock client that locks at session start, hands the lock to the
shell over a runtime socket without an unlocked instant (abandon + restore),
and retakes it the moment the shell heartbeat drops. Spartan PIN fallback
surface, PAM via hand-rolled libpam FFI, machined-style sync server.
Shell side: SessiondBridge singleton + LockScreen initIfReady routes the
startup lock decision through the handshake.
Dimmed now actually lowers the backlight (brightnessctl save/restore)
and Active restores it - previously dimRequested had no consumer, so
auto-dim never existed on this stack. Keep System Awake is checked
inside the monitor handlers rather than bound to enabled: recreating
the ext-idle-notify object during lock teardown is a fatal Wayland
protocol error on the Pixel compositor. Defaults now mirror the
hypridle timers (dim 120s, lock 300s).
The drag ghost was anchored (centerIn) — Qt won't move an anchored
drag.target, so no DropArea ever fired and stack creation died silently;
position it on press instead. Drag lifecycle now sets dockDragInProgress
(the DockManifest guard finally has a writer). Restored pulseDockReveal,
lost in a refactor while its osk.pulseDock caller survived.
Rail lifts atop squeekboard by measured layer height (hyprctl -j layers,
namespace osk); swipe down dismisses the keyboard first, then a visible
dock in any state (new dockSuppressed, cleared by swipe up). Overview:
taps in the grid's dead space dismiss it instead of being eaten.
DockConfig grew the real pins/stacks editor (unpin, rename, unstack,
dissolve).
Config.qml regained sidebar.width/widthExtended (their absence collapsed
the right sidebar PanelWindow to 1px — 'unclickable' on the laptop), plus
lock.dpmsTimeout, lock.unlockHook, and autoIdleInhibit with its Idle.qml
property. Audio.qml's absent capture source is now a zero-volume stub so
stock consumers stop dereferencing null, and the pactl status parse runs
under LC_ALL=C. volumeMixer dialog content comes along for the sidebar.
Findings logged in SouveraineOS/docs/DUMP-power-idle-lock-2026-07-15.md.
Phase 1 of the SouveraineOS homogenization — the three highest-priority
gaps from the session trust architecture:
New files:
- services/SessionEvents.qml — logind event ingress (PrepareForSleep,
session Lock signal) via gdbus monitor; delay-mode sleep inhibitor
held from startup, released only after WlSessionLock.secure
- services/StepUpAuth.qml — step-up PAM authentication with short-lived
in-memory grants per action family; revokes on lock/timeout/failure
- TRUST-BOUNDARY-MATRIX.md — every session operation classified by
caller type, auth gate, and enforcement status
Modified:
- IdleCoordinator: extended with Suspending/Asleep/Waking states for
the sleep lifecycle; wires to SessionEvents when present
- Session.qml: added sleep inhibitor kind, stepUp/sleepInhibitor
state in IPC projection, boot-time IPC audit log
- Config.qml: added lock.stepUp config section (enabled, grantTtlMs)
- LockConfig.qml: added step-up authentication settings section
- deploy.sh: added SessionEvents and StepUpAuth to manifest
- SESSION-TRUST-ARCHITECTURE.md: marked phase 1 items as done
- REFERENCE-EXTRACTION.md: marked build order items as done
Grows Souveraine's own surfaces on top of the borrowed ii shell and drops
the separate pill shell in favor of one integrated navigation rail.
Session arbiter (functions/Session.qml): probe logind's Can* methods over
busctl instead of guessing from installed binaries -- the answer carries the
polkit tier (yes/challenge/na), so a swapless phone reports hibernate as na
and refuses honestly rather than firing a verb that no-ops. Verbs run through
a Process that logs exit codes and tracks lastAction; refusals log too. The
busctl output is parsed with awk, not a sed regex buried under four escaping
layers -- the sed version returned nothing on the phone and left every
capability stuck at "unknown" (invisible on the laptop, where timing masked
it). Every structured result is JSON-over-string; quickshell maps a var
return to void.
Lock trust: screenLocked (the shell's lock request) is now distinct from
screenLockSecure (WlSessionLock.secure, the compositor's acknowledgement,
mirrored from LockScreen). Cards that disclose personal data gate on secure,
not on a button press. LockContentPolicy centralizes the ambient/personal/
step-up tiers so no card grows its own private rule.
New first-party namespace modules/souveraine/: LockMediaCard, LockSurfaceHost,
SystemGestureRail -- owned surfaces, not ii patches. IdleCoordinator gives one
staged idle vocabulary (dim/lock) gated behind nativeCoordinatorEnabled, off
until the native Wayland idle-notify is verified on the Pixel compositor;
hypridle stays the adapter. WallpaperAssets selects aspect-aware variants for
phone-vs-laptop display shapes.
Pill retired: pill/shell.qml and PillConfig gone, replaced by NavigationConfig
and the gesture rail. Hyprland starts qs -c souveraine directly; no secondary
shell, no qsConfig flip.
Verified on the phone: session.* reports challenge/na correctly, hibernate
and unlock refuse, inhibit round-trips with its reason.
Fork ii's Session singleton and add a sessionctl.* surface.
The IPC bug is the important half. Quickshell marshals exactly five types
(string/int/bool/double/color) and maps a `var` return to VOID, discarding
the payload with no error -- src/io/ipc.cpp, "void and var get mixed by qml
engine". dock.*, shell.* and apps.* were all declared `: var`, so they
registered as `(): void` and returned nothing at all. The {ok, reason}
contract has never once reached a caller. All of them now return JSON as a
string, which is what actually crosses the socket.
Session: upstream fires `systemctl X || loginctl X` detached and throws the
exit code away. Fine on a desktop with someone at the keyboard, not fine on
a phone where the shell is the session manager and a verb that silently does
nothing leaves you believing the machine is suspending when it isn't. So:
probe loginctl/systemctl/hibernate once instead of assuming, run verbs
through a Process that logs the exit code, and refuse honestly when the
machine can't do the thing (the phone has no swap -- hibernate now says so
instead of no-opping). Every upstream verb keeps its name and call sites.
Inhibits carry a mandatory reason and get a cookie; state() lists who is
holding the machine awake and why. "Why didn't it sleep" is now answerable.
unlock() is refused by design -- the lock is the credential gate, so no IPC
caller routes around the PIN pad.
Named sessionctl, not session: ii's SessionScreen already owns "session",
and quickshell drops duplicate targets silently rather than erroring.
Idle: drop the 2>/dev/null and run hypridle through a Process, so a unit
that fails to come back is a log line instead of a flat battery.
Verified on the laptop: inhibit stops hypridle, uninhibit brings it back.
Own config name composed by deploy.sh from our files + pristine-ii dir
borrows; ii tree stays untouched. One SouveraineFamily, two modes gated
on souveraine.phone.
Three guarded IPC surfaces for the agent (via Souveraine's harness, not
a new integration): dock.manifest/pin/unpin/restack, shell.surfaces/state
(layer registry, live-derived), apps.list/get/find/categories (.desktop
scan). Refusals are result shapes, not errors.
Settings app split into per-domain pages (Device/Lock/Dock/Pill/Keyboard).
Config backports (background.widgets, claudeUsage, fileSearch) kill the
laptop TypeErrors.
The OSK is layershell and can't rise above a session lock, so the lock
surface carries its own keypad. Lock state persists to states.json so a
qs crash while locked comes back locked; with lock.launchOnStartup the
lock screen is the greeter (greetd autologins straight into Hyprland).
Keypad gated behind lock.touchKeypad so desktop deploys keep the stock
surface.
Dock fan-out stacks, drag-to-combine, pill gesture rewrite, and the ii
patch set (TaskbarApps stacks API, Config dock.stacks schema) — pulled
from the live phone and made canonical here. deploy.sh grew a manifest
and a --phone mode: rsync the surface over, symlink ii into it, so live
edits land in a git tree instead of drifting.