Watch
1
0
Fork
You've already forked souveraine
0
Commit graph

484 commits

Author SHA1 Message Date
Fimeg
95dd21b998 shell: take the instance signature from viewtop, not from Hyprland's env
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.
2026-08-04 19:19:31 -04:00
Fimeg
6b67512c4f sensord: stop claiming the accelerometer
Measured on the phone, idle, screen on, same session, one flag apart:

  monitor-sensor --proximity --light --accel   iio-sensor-proxy  15.3%
  monitor-sensor --proximity --light           iio-sensor-proxy   1.1%

Claiming the accelerometer makes iio-sensor-proxy poll the IIO device
continuously; nothing else here does. Fourteen points of a core, forever,
for the reading §4 weights least (+0.3) and calls a weak signal — and one
the keepalive had to actively decay, because monitor-sensor speaks only on
orientation change, so a stationary phone paid the full poll cost to
report nothing.

The parser, the motion decay and MOTION_WINDOW go with it; all three
existed only to turn orientation into a motion edge. The 2026-07-27
finding behind the decay is kept in comment — 165 Moving(true) against 8
Moving(false) on one boot — because whatever reports motion next owes a
decay too.

This does not say accel is unwanted. It says a subprocess holding a
continuous claim is the wrong way to get it; TASK-36's SLPI batching is
the right one. Restore the source together with that, or the cost returns.
2026-08-04 16:17:21 -04:00
Fimeg
f320a0221d shell: answer to the ipc target sessiond actually calls
Action::Overview shells out to `ipc call overview toggle`. The only handler
here was `search`, so that call reached nothing and the three-finger tap
did nothing, without an error anyone would see.

It has been invisible from the device because the shipped sessiond has no
`gesture` verb yet and refuses the op long before the executor runs, so
only the first half of the break was observable.

This makes the existing binding reach the surface it already names. What
the tap should raise is TASK-55 Q1 and is not decided here.
2026-08-04 15:02:34 -04:00
Fimeg
44a54330c2 shell: mission control draws the cards it was already building
The panel, its mask and the loader all moved to overviewOpen ||
missionControlOpen when mission control landed; the Column holding them
did not. So the second-stage pill swipe raised a panel whose entire
contents were invisible — WindowOverview instantiated, laid out, and never
shown.

Search stays overview-only: mission control takes no keyboard focus on
purpose, so a field there is one you can see and cannot use. Outside-tap
now clears both states, because with no focus grab that tap is mission
control's only way back.
2026-08-04 14:43:35 -04:00
Fimeg
1a3c24fadb claude: fix Opus 5 wire incompatibilities
Drop temperature for models that removed sampling params (400 otherwise).
Preserve signed thinking blocks across tool rounds — Anthropic rejects a
tool_use whose preceding thinking block was dropped. Raise default
max_tokens to 32k for thinking-by-default models and request summarized
display so the reasoning pane is fed.
2026-08-04 11:27:13 -04:00
Fimeg
d0c6488099 deploy: WindowOverview is in the qmldir and was not in the manifest 2026-08-04 09:39:00 -04:00
Fimeg
356cfc6462 overview: viewtop has workspaces; the header said it did not 2026-08-04 09:32:03 -04:00
Fimeg
914e48f2d3 shell: locking closes the keyboard, and unlocking does not bring it back
The hold machinery remembers whether the keyboard was up when the first
hold was taken, so releasing the last hold restores rather than clobbers
it. Across a lock that restore is wrong: the keyboard was up because of
something being typed before the phone went down, and the first thing
you see on unlocking is a keyboard sitting over whatever you came back
for.

Intent to type does not survive the screen going away. Anything that
still wants the keyboard — a field taking focus again — asks for it, on
the path that already works.
2026-08-03 22:20:51 -04:00
Fimeg
cc4bb83573 shell: the overview arrives as one surface, and mission control finally lands
Everything drove off a visible/not-visible flag, so the cards appeared
rather than the overview opening. One shared `progress` now: the strip
slides 24px, each card takes its own share of it staggered by index and
capped at the fifth, and scale runs 0.86 to 1.0 — the reference shell's
numbers, driven from the same clock instead of a timer per card. The cap
is what stops the tenth card starting half a second after the first,
which reads as loading rather than opening.

This is also SHELL-ECOSYSTEM's stated model, taken from Phosh: state
gates visibility, never the reverse.

`missionControlOpen` has been set by the pill's second-stage swipe since
July and consumed by NOTHING — TASK-14 records the Auxo-like card
surface it was meant to raise as never built. WindowOverview is that
surface. The state reaches something now instead of being set and
dropped.

Mission control is the cards alone: no search, and no keyboard focus,
because it is "switch to what is running" and not "find something" — and
taking the keyboard would summon the OSK over a surface with no field.
2026-08-03 22:18:43 -04:00
Fimeg
133daa94ef sessiond: the wake latch is per button, not one field for all of them
An audit found the fix from earlier tonight reintroduced through a
second button. A tap never resolves on the UP edge — the recogniser
returns None and the tap lands when the 300 ms multi-tap window closes
on a later tick, up to about 1.3 s after the press — so a single shared
latch had to survive that whole gap, and any other button's DOWN edge
inside it overwrote the answer.

Concretely: power wakes a dark phone, the volume rocker under the same
grip gets nudged within the second, and the pending power tap resolves
as "the panel was lit" and blanks the screen. That is exactly the
regression 8c7567e was written to fix.

`buttons` is already keyed per button so they cannot interfere; this
latch had reintroduced the coupling one level up. The test fails without
the fix — checked by reverting it.
2026-08-03 21:12:36 -04:00
Fimeg
91ea54ebf5 sessiond: touch gestures are a verb, so the compositor stops deciding
viewtop recognises three fingers and had nowhere to send it. The obvious
move — have the compositor call `qs ipc call overview toggle` itself —
is the eighth blind actor §12 forbids by name, and wiuf-vpn-gate is what
that costs: 652 tunnel recycles in ninety minutes with no way to turn it
off.

So `gesture` joins `button` on the socket. The asymmetry between them is
the interesting part and is written down where it happens: `button`
carries raw edges because recognition is an accumulation over time and
the machine owns time, while `gesture` arrives already named, because
touch contacts exist only inside the compositor and nothing else CAN
recognise them. The line §12 draws still holds — the compositor names
what the fingers did, the machine decides what it means, and behaviour
leaves through the one executor table.

Three fingers raise the overview. Everything else is recognised,
recorded and inert, which is deliberate: a gesture that fires something
nobody chose is worse than one that fires nothing. The real binding
table belongs in DeviceStatePolicy, persisted and agent-writable.

Not on a dark panel. The overview is content, and content on an
unauthenticated glass is what the disclosure rules exist to prevent — a
tap on a dark panel is a wake, and that is the power button's business.

The reply says whether the gesture was bound, so a caller can tell "the
machine had no binding" from "the machine did not understand".
2026-08-03 20:53:18 -04:00
Fimeg
8c41f45420 shell: the overview swipes sideways, and stops rejecting its own buffer
Two faults in the first cut.

cacheBuffer was height * 2, and height is -1 until the first layout
pass, so ListView refused it outright and said so on every open. Clamped
at zero.

The strip was vertical. TASK-14 asks for "horizontally swiped live app
cards" and the reference shell's carousel says the same thing in its own
comment — the whole strip slides in horizontally. On a phone the thumb
travels sideways, and a vertical list fights the flick-up that dismisses
a card, so the two gestures were competing for the same axis. Horizontal
with snap-one-item, cards filling the viewport.
2026-08-03 19:15:18 -04:00
Fimeg
fdb0acfdce shell: the overview shows windows, because there are no workspaces here
OverviewWidget draws a grid of workspaces and places windows in them by
Hyprland coordinates — HyprlandData.windowList, monitorData,
Hyprland.monitorFor. Under viewtop none of it resolves, and more to the
point viewtop has no workspaces at all: one space, a tiling layout. The
old overview could not be repaired by repointing it at another data
source, because the thing it draws does not exist. It rendered an empty
frame, which reads as the overview being broken.

WindowOverview lists what does exist. Cards from
ToplevelManager.toplevels — the same list the dock and TaskbarApps
already read, so there is one idea of what is open — each holding a
live ScreencopyView of one window, which only became possible when the
compositor started serving a per-window capture source; until then a
card could have shown nothing but the screen it was covering.

The motion is the reference shell's, read out of
overview_window_card.dart rather than invented: stagger at index * 45 ms
capped at the fifth card, intro scale 0.86 to 1.0, dismiss past 32% of
the card height, 56 px of downward rubber-banding, settle under 200 ms.
The cap is the part worth keeping — without it the tenth card starts
half a second after the first and the overview feels like it is loading
rather than opening.

Captures run only while the overview is up. A live capture per window is
a render of that window every frame, and leaving them going behind a
closed overview is battery spent drawing what nobody can see.
2026-08-03 18:37:56 -04:00
Fimeg
c21bd1d7d9 shell: stop gating the dock on a flag the keyboard flaps
Suppressing the dock whenever oskOpen was true, above effectivePinned so
it applied in every state, hid the dock permanently on the device.

oskOpen is not "the keyboard is on screen". GlobalStates' own comment
says squeekboard hides itself whenever input-method focus drops and that
a hold re-asserts it, and the journal shows exactly that: self-showed /
self-hid every couple of seconds, settling at Visible=true with no
keyboard in front of the user. Gating a persistent surface on a flag
that flaps turns a cosmetic overlap into a dock nobody can reach.

The empty-desktop check keeps its oskOpen term, where it always was: a
spuriously-true flag there costs a reveal that was cosmetic anyway,
which is a different price from hiding a pinned dock.

The real signal is the keyboard's exclusive zone, which the compositor
already applies — an unpinned dock declares zone 0 and is placed above
the keyboard for free, the same mechanism that fixed the pill. The
pinned case, where both reserve space and the reservations stack, wants
fixing where the zones are arbitrated.
2026-08-03 17:17:01 -04:00
Fimeg
ca44b0be2a shell: the keyboard owns the bottom edge, and nothing measures it
Two bugs with one cause — the bottom edge was being reasoned about
twice, by hand, against a compositor that is gone.

The rail set ExclusionMode.Ignore (exclusive zone -1: ignore what
everyone else reserved, anchor to the whole screen) and then computed
its own lift from the keyboard's height, probed by shelling out to
`hyprctl -j layers`. Under viewtop there is no hyprctl, so every probe
threw a JSON.parse on empty output and logged it in a loop every second
or two, forever, while the margin stayed at a hardcoded fallback
measured against a keyboard that is not the one running. A 200px
fallback against some other height is exactly "the pill spawns in the
middle of the keyboard". Zone 0 with Normal — the idiom SidebarRight and
ReloadPopup already use — reserves nothing and respects what others
reserved, so the compositor anchors the rail to the top of the
keyboard's exclusive zone. Correct at any height, any rotation, across a
swap, with nothing to go stale. The probe, its retry timer and
probeOskHeight() are deleted rather than ported: a second measurement of
a number the protocol already carries is what this class of bug is made
of.

The dock consulted oskOpen only in its empty-desktop branch, below
effectivePinned, so a PINNED dock ignored the keyboard and sat under it.
DockManifest.dockState() has always reported "suppressed-by-osk" and
_blockReason() has always refused mutations with "osk-open" — the
manifest promised a suppression the dock never performed, so the agent's
view of the screen and the screen disagreed. The check moves above
pinning and applies in every state, staying below dockRevealPulse
because an explicit reveal is the one thing that should outrank the
keyboard.

Verified on the phone: the probe loop is gone from the journal. The
resulting pill and dock positions want eyes on the glass.
2026-08-03 16:53:06 -04:00
Fimeg
1f028c129e sessiond: a one-second proximity blip no longer flaps the state
The phone's trail was almost entirely Locked -> Observed -> Locked. The
snapshots said why: prox=true on the way in, prox=false one second later
on the way out, over and over. `near` must hold 700 ms to be believed
and `far` is believed instantly, so a one-second episode is long enough
to enter and its end is immediate.

The debounce cannot be where this is fixed. suppress_wake reads the
debounced value, so slowing `far` there would keep vetoing tap-to-wake
after the sensor was uncovered — which is exactly what
PROXIMITY_FAR_DEBOUNCE's zero exists to prevent. One reading, two
consumers, opposite needs: the veto wants far fast, the state wants it
stable. So the hysteresis is on the state and the veto keeps its instant
edge, with a test pinning that separation.

3 s, bounded by data already in this file rather than by feel: the
measured blips ran ~1 s, and of the 44 recorded episodes the nine real
ones all ran >= 5 s. It sits above the noise and below every genuine
episode.

It reduces the flapping rather than abolishing it — a sensor that keeps
blipping still enters Observed on each 700 ms near. Raising the entry
bar needs the same split applied to the near edge, which is a second
change with its own justification to earn.

Two existing tests asserted the instant exit and now advance past the
dwell. The property they are named for is unchanged: far ends Observed.
2026-08-03 16:03:36 -04:00
Fimeg
6cb245492e sessiond: record the wake, so the trail can tell it from nothing
apply_gesture's dark-panel branch returned Unblank+Restore without a
record_decision, while the blank branch records panel-off or
lock-before-blank. The trail showed 'button-gesture recognised' and then
silence, so a press that woke the panel and a press that was swallowed
left identical evidence — which is the one question anyone debugging a
dark phone is asking.
2026-08-03 15:16:09 -04:00
Fimeg
8c7567e419 sessiond: the press that wakes the panel must not also blank it
Press the power button on a sleeping phone: the screen comes on, the
lock screen appears, and it goes black again.

One physical press produces two reports and the second cannot see what
the first did. note_input fires on the DOWN edge — correctly, a finger
on the button is a user present — which takes the device out of Locked;
the executor lights the panel and reports it back through set_panel
before the finger is off the button. apply_gesture then resolves the tap
on the UP edge, reads panel_on as true, and takes the lock-then-blank
branch. The press that woke the screen is read as a press to blank it.

The panel state is latched on the DOWN edge and the gesture is decided
from that. The level is unreadable by the time the gesture resolves;
only the edge is still true. Sixth edge-versus-level bug here, after
locked_ack, ChargeRate, bootBloomActive, hasLoginctl and the compositor's
dormant OR in disclosure_locked.

The existing dark-panel test passed throughout because it never
simulated the executor's report landing between the two edges. The new
one does, and fails without the latch.
2026-08-02 22:20:13 -04:00
Fimeg
266af98a99 sessiond: double-tap-to-wake wakes, and the volume keys work again
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.
2026-08-02 18:28:49 -04:00
Fimeg
d2a0547d30 ci: build sessiond for x86_64 too
It was skipped with a comment calling it deliberate; it never was — the
laptop hit lock-screen errors once and it stayed phone-only by inertia.
Building is not enabling: the binary ships, the user unit stays
aarch64-only in PKGBUILD.prebuilt, so this cannot wedge a machine by
arriving on it. Verified it compiles.

Not urgent — the laptop is not SouveraineOS yet — but the comment was
wrong and a wrong reason is worse than no reason.
2026-08-02 17:49:47 -04:00
Fimeg
1f1fc5c7dd sessiond: put the panel doc comments back on their own functions
A scripted edit stacked three doc comments onto panel_on() and left
set_panel() with none. Comments are load-bearing here — set_panel is the
report half and request_screen is the ask half, and the whole reason both
exist is that a report must not actuate and an ask must not silently edit
the machine's idea of the hardware. That distinction was sitting on the
wrong function.
2026-08-02 17:36:48 -04:00
Fimeg
b45fb705db sessiond: the machine can wake the panel, and so can she
The state machine could turn the screen off and had no way to turn it
back on. `apply_gesture` answers a power tap on a dark panel with
`Restore` — which is brightness — and there was no unblank in the action
table at all. That was invisible because the wake never came through the
daemon: hyprland.lua bound the physical key straight to
blueline-screen-toggle, so the compositor woke the panel and sessiond
only heard about it afterwards.

viewtop consumes that key on purpose — the control that locks the phone
must not be interceptable by whatever is fullscreen — so the gap became a
phone that could sleep and never wake. Measured on hardware: panel off,
presses arriving and logged, no way back short of ssh.

Action::Unblank closes it, and a power tap now returns [Unblank, Restore]
in that order, because the brightness a panel comes back at means nothing
until the panel is back.

Also a `screen` verb, because a wake bound to a keybinding in a
compositor config is not something the agent can reach and §13 says that
is a defect. It is not a bypass: `on` is immediate, `off` routes through
request_blank() like every other path, so she can turn the screen off and
still cannot darken an unlocked session. Operation is hers; the ordering
is a property of the machine, not a permission she lacks.

100 tests, four new ones pinning exactly that.
2026-08-02 17:29:33 -04:00
Fimeg
106c3bb680 lockhint: say so when the session can never hold a lock hint
logind refuses SetLockedHint for any session that is not Class=user. When
that happens LockedHint is pinned at no, is_locked() is permanently false,
every request_blank() times out its ack budget, and the panel goes dark on
a session nobody could confirm was locked — the machine keeps working and
only the security half is gone.

That state ran for weeks on blueline undetected: greetd's default_session
is the greeter slot, so any restart produced it, and the only trace was
one blank-without-lock per blank, which reads like a timing problem rather
than a structural one. Now it is one line naming the cause at startup.

A warning, not a refusal to start: a phone that will not boot because its
session class is wrong is worse than one that boots and says so.
2026-08-02 15:44:43 -04:00
Fimeg
94988138b4 lock hint: resolve the seat's session, and replay the report
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.
2026-08-02 15:26:37 -04:00
Fimeg
5edf0c410e sessiond: create lock surfaces before the verdict, not after it
`ensure_surfaces` returned early unless `state.locked`, and `locked` is
what the compositor sends once lock frames are up. ext-session-lock-v1 is
explicit: "The locked event must not be sent until a new 'locked' frame
has been presented on all outputs." So a compositor honouring the spec
waits for the surfaces this function refused to create until it had heard
from the compositor. Neither side can move.

Hyprland hides it by acking `locked` before any lock surface exists,
which is why this stood. Measured against viewtop on blueline today:
sessiond sat in `blocking_dispatch` waiting for a verdict that could not
arrive, and the session crash-looped every eleven seconds.

The wait loop was also deaf to the control channel for its whole 60s
budget, so a shell registering during it got `lock session did not
release in time` (server.rs waits 5s) and then asked the compositor for
a lock this thread still held — TASK-48's crash, reached from the daemon
side. It now drains Release before each dispatch and draws while it
waits, which is the order the protocol describes.

408 tests pass.
2026-08-02 11:24:46 -04:00
Fimeg
870d88210b shell: wait longer for shell_ready than sessiond takes to answer it
`shell_ready` blocks in the daemon for up to 5s waiting for its
lock-session thread to drop its Wayland connection, because the
compositor refuses a second locker while the first is alive
(server.rs `wait_timeout_while`). The shell's timer was 3s, so it gave
up on a handshake sessiond was still answering, assumed locked, and
asked for a lock that had not been released — straight into TASK-48's
"Tried to show lockscreen surfaces without active lock".

Under Hyprland the release usually lands in milliseconds and the race is
won. Measured against viewtop on blueline today it loses every time: the
shell crash-looped every 11 seconds and the session never came up.
Same latent bug, a compositor that exposes it.

7s, so the daemon's own deadline expires first. Its answer for that case
is a refusal (`lock session did not release in time`), and hearing a real
refusal beats inventing a verdict — "assuming locked" is the shell
holding state the authority owns.

The 5s retry timer only arms after this one fires, so nothing overlaps.
2026-08-02 11:15:44 -04:00
Fimeg
d2f51e93de sessiond: a working bearer action is a decision, not an error
Both executors recorded success via record_error, so the daemon's own working
actions landed in the trail's error channel - visible on the phone as
{"event":{"error":{"component":"bearer","error":"preferred wifi"}}}. That makes
the one signal a reader scans for useless.

They also discarded every exit status, so a failed nmcli was indistinguishable
from a successful one and the trail would claim a metric that was never set.
Both now check status: record_decision on success, record_error with what
actually failed otherwise. The pin failing is expected without CAP_NET_ADMIN
and says so - the tunnel still works, it just follows the default route
instead of the chosen link, and those are different states.
2026-08-01 15:10:38 -04:00
Fimeg
7e91de1956 sessiond: never reapply clat — it destroys the carrier's own routes
Measured on the phone. After one PreferLink the clat device was up with its
192.0.0.1/32 address and *zero* routes: no default, and no 205.151.11.13/32,
which is the only path to the MMS proxy. ip route get 205.151.11.13 went out
wlan0. Restored by restarting blueline-clat.service.

nmcli device reapply resets a device to its connection's config, so it is
destructive to precisely the routes a sidecar daemon owns - and the CLAT
daemon owns all of clat's. TASK-49 acceptance #2 says carrier services ride
the carrier; this broke it on the first action it ever took.

The gsm connection was the wrong target too. The modem is v6-only and the v4
default for cellular is installed by the CLAT daemon as
'default dev clat scope link metric 2048', not by the gsm connection, so a
route-metric set there is a number nothing reads. Wifi's metric is the only
lever: under 2048 wifi wins, over it the carrier does. One connection
modified, one device reapplied, and the carrier's routes left alone.
2026-08-01 14:58:34 -04:00
Fimeg
ac084b461a sessiond: read tunnel health from sysfs, not wg show
Found on the phone: the bearer verb reported tunnel "off" while wiufph was up
and handshaking (4.11 KiB received). wg show needs CAP_NET_ADMIN for
WG_CMD_GET_DEVICE and sessiond is a systemd *user* unit, so it gets "Unable to
access interface wiufph: Operation not permitted" and every tunnel reads off,
forever. A daemon that cannot see the instrument it is built around.

/sys/class/net/<if>/statistics/rx_bytes is world-readable and is the same
number without the parse or the privilege; DEVTYPE=wireguard in uevent finds
the interface, and NM removes the link entirely when the profile goes down, so
its presence is the up/down signal.

The endpoint lookup had the same defect. NetworkManager hands wireguard.peers
to an unprivileged caller, and the endpoint is configured as a hostname
(sub.wiuf.net:51822), not the address the old gate hardcoded - so it is
resolved before pinning, and a failed resolution writes no pin rather than a
wrong one.
2026-08-01 14:44:32 -04:00
Fimeg
0f7fe27c62 ci: re-run; rust-test 156 failed with all three of its commands green on 1.94 2026-08-01 14:21:12 -04:00
Fimeg
f6479c98a1 sessiond: make home_ssids and bearer_settle settable
A policy field with no verb behind it is unreachable. set_policy takes both,
get_policy reports both, and a zero settling window is refused the same way a
zero lock-ack budget is - it is the event-speed controller by another name.
2026-08-01 14:09:38 -04:00
Fimeg
4fd3d244ab sessiond: bearer posture as evidence and an Action (TASK-49)
Nothing owned which link carries traffic. It was the residue of a DHCP metric,
a NetworkManager penalty and a shell script's assumption, and when those
disagreed the phone silently lost the network - every IPv4 destination leaving
over a degraded clat while wifi sat associated.

The thing this replaces, wiuf-vpn-gate, is worth stating because its three
defects are the design constraints:

- it was driven by the NM dispatcher AND a 90s timer while calling nmcli
  up/down, which is itself an NM event. 652 tunnel recycles in 90 minutes,
  ~7/min - a loop, not a bad interval. Here the probe only reads, the decision
  is a pure function, and the only egress is an Action from tick().
- its home test was ' inet 10.10.' - a /16 match that reads the foreign network
  this phone lives on (10.10.30.213) as home. home_ssids is identity; an empty
  list means never claim to be home.
- it managed CONN=wiuf while two profiles answer to that name.

LinkHealth keeps 'associated' apart from 'carrying', which is DEVICE-STATE-
MACHINE §10's source-health lesson applied to links: a wifi link that
associates and cannot route is a sensor that heartbeats and lies, and it is
what NM's +20000 penalty mislabels. A carrying cellular link therefore beats a
merely associated wifi one; wifi only wins ties.

TunnelHealth reads wg's received-byte counter, which nothing read before. Up
and deaf is a real state - measured over clat as 368 B received against 5.30
KiB sent - and every other readout on the device calls it connected.

Anti-flap is a settling window, not a rate limit: a changed preference must
hold BEARER_SETTLE (20s) before it produces a command, and the clock restarts
whenever the candidate itself changes. A steady state emits nothing, so it is
safe to evaluate every tick. Tests cover the flap that never settles, the
settled state that must not re-emit, and the dead network that must not be
flailed at.

NetworkManager stays the single writer of routes (acceptance #6): preference
is expressed as ipv4.route-metric plus device reapply, never ip route, and
reapply rather than up/down so a correction is not itself an NM event. The one
route sessiond owns is the WireGuard endpoint /32, which NM has no opinion
about.

The tunnel toggle stays the user's. tunnel_underlay() returns None when the
tunnel is off - this decides how traffic is carried, not whether the tunnel
exists.

Probed off the existing clock every 5th tick, with the lock released; no new
timer (acceptance #6) and no subprocess spawned under the state mutex.
2026-08-01 14:08:14 -04:00
Fimeg
a89556aef7 packaging: ship souveraine-button, the reporter that replaces the policy script
Reports down/up edges to sessiond and decides nothing. Wire it in
hyprland.lua as a bind/bindr pair — reporting only the press leaves the
machine believing the button is held forever.
2026-07-31 21:48:08 -04:00
Fimeg
86afe00dba sessiond: the power button reports edges; the machine recognises gestures
blueline-power-button decided policy in shell script — read its own panel
state, asked the shell to lock over qs ipc, polled, then blanked the panel
itself. That was a path to a dark panel outside request_blank(), so §1's
lock-then-blank invariant had a hole in the most-used control on the device.

Edges in, gestures out: tap, double, triple, hold, long-hold, per button.
Only the power tap is bound; the rest are recognised, recorded and inert
until there is a binding table to point them at.
2026-07-31 21:45:47 -04:00
Fimeg
5c618f1514 lock: hoist the lock out of the Config.ready loaders so a reload can adopt
PanelLoader is a LazyLoader gated on Config.ready, behind a second one.
On reload both are shut when quickshell propagates, so LazyLoader has no
item to hand its successor and the new WlSessionLock never adopts the live
compositor lock. Lock is a direct child of ShellRoot now, as the boot-order
doc already asked.

Ungated means Config may not have loaded, and touchKeypad reads false from
the adapter defaults — which would bind the desktop surface on the phone and
cannot be changed once the lock is active. The choice rides the reload too.
2026-07-31 21:25:05 -04:00
Fimeg
1eb50a1817 sessiond: subscribe — the body tells the mind when a belief changes
A held connection receives one trail entry per notable edge: transitions,
source-down/recovered, violated guarantees. Readings, wakes and heartbeats
never cross — the filter is the point, not the transport.
2026-07-31 21:07:19 -04:00
Fimeg
b5ee87da0f ci: sessiond clippy is advisory until its 9 pre-existing warnings are burned down 2026-07-31 19:14:27 -04:00
Fimeg
539df90b95 sessiond: peer pid via getsockopt, and put sessiond under test
UnixStream::peer_cred is still unstable, so it only failed at the aarch64
build after rust-test had gone green. Use the same getsockopt machined
uses.

rust-test never built souveraine-sessiond at all — it is
required-features and the feature is not default. Build and test it.
2026-07-31 18:52:43 -04:00
Fimeg
b5fc48eef1 ci: one branch. primary builds and ships; public and the dead codeberg mirror are gone 2026-07-31 14:36:46 -04:00
Fimeg
efe2d0d1e0 lock: adopt the session lock across a scene reload
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.
2026-07-31 14:32:29 -04:00
Fimeg
0745b48dc5 vpn toggle: status, not a bool
It read "On" for an hour through a dead tunnel — NM said activated while wg had
368 B received against 5.3 KiB sent. Now off/connecting/limited/online, decided
by a 2s probe of the far-side resolver rather than by the interface being up.
Sailfish models the same distinction (Online/Limited/Connecting); Android won't
let an unvalidated network win at all.
2026-07-31 13:57:29 -04:00
Fimeg
189de61cbe fix: guard altAction on long-press
GroupButton called it unguarded while releaseAction two lines up was guarded.
altAction is null for every toggle without a menu, so a long-press threw. Only
showed once the toggles that had been filtered out of the panel started
rendering.
2026-07-31 13:38:25 -04:00
Fimeg
1c90d7f76a settings: the device state machine gets a surface
TASK-08(f)/TASK-19: state, panel, evidence, confidence, per-source health, the
sensors_degraded flag and the recent decision trail were legible only through
forensic.jsonl. DeviceEvidence gains a read path (polled only while watched);
the Device page renders it. Readout only — the confidence gates are still
computed and never branched on, so controls over them would lie.
2026-07-31 13:11:43 -04:00
Fimeg
8a4eebbdd4 deploy: never --delete into the phone's tree
It is not a git repo, it gets edited on the device, and this path had no
snapshot while the local compose path has had one all along. Now it snapshots
to ~/souveraine-surfaces/.quickshell-previous, copies by checksum, and only
reports phone-only files instead of destroying them.
2026-07-31 12:58:52 -04:00
Fimeg
49635fb005 shell: give the subconscious event log a host
Tier 2 was written, deployed and mounted by nothing — its own header named the
gap. The chat sidebar hosts it now: ticker long-press, or the psychology
indicator by the input box, which works when no pass is running.
2026-07-31 12:51:17 -04:00
Fimeg
e6e3dd3b2a shell: quick toggle types come from the chooser, not a second list
The hand-written allowlist in AndroidQuickPanel had drifted from it, so the
phone rendered 12 of its 17 configured toggles and said nothing about the rest.
Adds the missing fullScreenshot and waydroidTerminate, and a deploy guard so
the phone chooser can't fall behind the base one again.
2026-07-31 12:51:10 -04:00
Fimeg
e4e659483b fix: keep the conversation when the agent list is re-polled
selectAgent cleared conversationId even when the agent hadn't changed, and the
60s inventory poll re-selects the persisted agent forever — so any message sent
a minute after the last one opened a fresh conversation. 27 of them yesterday,
four messages each. An empty curl body no longer counts as "no conversations".
2026-07-31 12:50:24 -04:00
Fimeg
e4d6ddcc72 vpn toggle: point at wiufph, the phone's own peer
The old wiuf profile shared its address and private key with the laptop, so
WireGuard could only hold one of them, and the key is no longer listed
server-side — it sent and never received. wiufph is 10.8.0.6, port 51822,
keepalive 25. Proven off-LAN: handshake 8s, gitea 200 in 0.126s.
2026-07-31 07:57:30 -04:00
Fimeg
0b57daac06 shell: VPN quick toggle in the right panel
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.
2026-07-30 19:13:19 -04:00
Fimeg
15af38ae36 deploy: ship the subconscious module in the shell manifest
The ticker instantiation in AiChat referenced qs.modules.souveraine.
subconscious, but those files were absent from the deploy MANIFEST, so a
deploy left the import unresolved. Add SubconsciousTicker/EventPanel +
a qmldir mirroring the dial/selection modules.
2026-07-30 15:26:20 -04:00