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

494 commits

Author SHA1 Message Date
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
Fimeg
4caca36859 fix: preserve the full reply (not the streamed partial) on surface-drop
Revises the prior fix: the model's reply is already complete in
final_content before the paced stream loop, so truncating to
needlessly dropped the unstreamed tail. On SSE receiver-drop, stop
streaming but commit the FULL reply (interrupt flag skips the pointless
N+1 and marks the turn); the surface recovers the whole reply on resume.
2026-07-30 15:22:12 -04:00
Fimeg
483902a40e shell: restore the subconscious ticker to the chat column
Re-mount SubconsciousTicker in AiChat (lost in the 07-30 relocation out
of the ii tree). The view is decoupled; it just needed its host back.
2026-07-30 15:17:55 -04:00
Fimeg
426dcf6363 fix: commit partial reply when the SSE surface drops mid-stream
turn.rs streaming loop returned early on tx.send failure (receiver gone)
before the session commit, orphaning the turn: the assistant's partial
reply never landed, so the next turn's history snapshot read the user's
follow-up with no preceding reply -> a user->user gap. The model then
answered as if from nowhere (the 'whatever it was...' non-sequitur),
model-independent and intermittent. Commit the partial like an interrupt
instead, the way the cancel branch already does, so the thread links.
2026-07-30 15:17:55 -04:00
Fimeg
3bc363a903 mirror souveraine + upower-souveraine into the Gitea Arch registry
Best-effort mirror step after the edge publish: PUT each built pkg to
/api/packages/Fimeg/arch/edge so the Packages page stays current. Never
fails the publish; 409 = bump to republish. Runs only on public (same
gate as the publish job).
2026-07-30 14:24:08 -04:00
Fimeg
25898a9d14 shell: re-home the subconscious surface from the ii tree to souveraine
The subconscious three-tier surface (SubconsciousTicker + SubconsciousEventPanel)
is Souveraine-original, not upstream ii. It lived under ii-base/modules/ii only
because it had never been re-homed, so the ii -> souveraine shell migration
silently stranded it: the files survived, but the running shell (qs -c souveraine)
never loaded them, and the feature dropped off the device.

Relocated into modules/souveraine/subconscious/, decoupled from the ii overlay
system it was riveted to:

- SubconsciousTicker: was clean of ii imports but opened the panel through the
  ii OverlayContext singleton. Now emits requestOpenPanel(); a host connects it.
- SubconsciousEventPanel: was a StyledOverlayWidget (qs.modules.ii.overlay). Now
  plain content a host instantiates; the pin/close/drag affordances were the
  overlay host's job, not the content's.

The model (services/Ai.qml) was already substrate-neutral and never moved; only
the views were stranded. GAP NAMED: no souveraine overlay host mounts these yet,
so they do not reach the glass until one exists — the ii overlay host was
intentionally not ported. The dangling ii references (AiChat, OverlayContext,
OverlayWidgetDelegateChooser) are removed.
2026-07-30 12:57:19 -04:00
Fimeg
0ef239515f surface: per-agent TTS voice + turn clock + reply-handler fix
Speech resolves voice per active agent (agents[].voice_id from the list),
with the system voice as fallback — two agents on a host need not match.
Souveraine.qml carries voice_id into the agents map and adds a turn clock
(turnStartedAt/turnElapsedMs) + a 60s agent-inventory refresh. Ai.qml
declares pendingFilePath and imports qs.modules.common.functions so
StringUtils.ttsClean no longer aborts the stream-finished handler.
2026-07-30 09:18:56 -04:00
Fimeg
e9bf33e64a agent: per-agent voice_id on the public list
AgentSummary carries voice_id, sourced from agent.json (_souveraine) via
get() rather than the DB config_json column, which is write-once at create
and drifts on later edits. The list endpoint is the surface's only
unauthenticated view of an agent, so voice travels there; /v1/agents/:id
is token-gated.
2026-07-30 09:18:38 -04:00
Fimeg
147f7f28b1 Selection: size the window to the chip instead of masking a fullscreen shield
The masked-fullscreen version was completely untappable on device. Rather than
keep guessing at the mask, the window is now sized to the card and moved by
margins, so every pixel of it is the chip. Also stops an invisible fullscreen
surface sitting over the app's own selection UI.
2026-07-29 18:13:02 -04:00
Fimeg
5a643d7059 Selection: fix untappable chip and the chip that never cleared
Two bugs from the first on-device run.

The card sized itself from a layout it also centered, which resolved to zero, so
mask claimed a zero-size input region: the chip painted but no tap landed. Card
now sizes from the layout's implicit size and the background is a plain Item
child, not layout-managed.

wl-paste --watch replays the existing selection on start, so every watcher
restart (every unlock) resurrected a stale selection that could never be cleared
because the primary buffer never changed again. The first emission after a start
now only sets the baseline, and a dismissed selection is remembered so a re-read
cannot revive it.
2026-07-29 18:05:49 -04:00
Fimeg
808f06358e Selection: import qs for GlobalStates
Deployed and verified on blueline: watcher starts, a selection settles with an
anchor, and the lock gate kills the watcher.
2026-07-29 17:54:39 -04:00
Fimeg
0bebc779c6 Selection menu surface: chip that expands to actions (TASK-18)
Overlay layer following DialHost, keyboardFocus None throughout, and an input
mask limited to the card so taps elsewhere reach the app underneath. Anchors to
the pointer hint above the touch point, clamped on screen.

Chip shows a character count, never a preview: the surface floats over the app
that owns the selection and the content may be a password, so it stays ambient
and discloses nothing. Read Aloud is live via Speech; agent and reference
actions render with the reason they cannot act yet.

Adds DeviceEvidence, reporting input to the state machine's existing
Request::Input with an intent label, so a new input surface is not another
isolated actor per DEVICE-STATE-MACHINE 1. Opt-in: nothing loads or watches
until Config.options.selection.enable.
2026-07-29 17:47:20 -04:00
Fimeg
147999a918 Selection service: compositor-wide primary selection for TASK-18
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.
2026-07-29 17:25:34 -04:00
Fimeg
c8507e924b sessiond: delete the fallback login; this daemon locks and never unlocks
It authenticated through PAM, called unlock_and_destroy(), and returned
Unlocked — and then the shell, whose GlobalStates.screenLocked is a separate
bool, re-registered, was told must_lock=true, and locked again. The protocol
has a lock directive and no unlock directive, so the fallback could open the
compositor's lock and had no way to tell the session it had. Casey hit it
exactly that way: "it logs in, and then qs says Locked still."

It was also not a last resort in practice. It is raised whenever shell_alive is
false, and that flag lied for 90 minutes on 07-29 because a scene reload's
re-registration was refused and the shell gave up (45fbbea). So it was the
first thing reached, on a bad signal, to do a job it could not finish — the
fallback shape the doctrine forbids.

Gone: auth.rs, the PIN pad, the keypad geometry and digit font, the
failed-attempt counter, SessionOutcome::Unlocked, Msg::AuthDone, and the
--pam-service plumbing down to the CLI.

Kept: acquiring ext-session-lock before any shell exists, which was never a
fallback but the ordering invariant from LOCK-DPMS-LESSONS.md §1, and the
Released handoff. A locked session with no shell now stays locked and says so
in the journal. Recovery is the shell returning, or a reboot.

79 tests pass.
2026-07-29 11:37:08 -04:00
Fimeg
0a635ff984 lock: dismiss the boot bloom on a level, not only on an edge
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.
2026-07-29 10:54:45 -04:00
Fimeg
45fbbea400 shell: "already registered" is a transient, not a verdict
A scene reload re-runs SessiondBridge while the outgoing connection is still
open, so the reload's shell_ready races its own predecessor's EOF and sessiond
refuses it — correctly, since the lease is genuinely held. The bug was the
shell then giving up for good: it read the refusal as "another shell owns the
lease, we are not the authority", cleared nothing, and left registerRetry
idle. A second later that EOF landed, shell_alive dropped, and sessiond spent
the rest of the session believing there was no shell — while a live one sat on
a connected socket.

Observed 2026-07-29: refused 09:14:35, predecessor closed 09:16:36, and
`{"op":"status"}` reported shell_alive=false for 90 minutes afterwards.

Now the refusal sets needsRegistration and lets the 5s retry run. If the lease
really is another live shell's, each retry is refused again for free — and we
take it the moment that shell dies. Verified on hardware: retrying (1) ->
registered -> locked_ack sent, sessiond back to phase=released.
2026-07-29 10:43:07 -04:00
Fimeg
de64480ba2 gestures: osk-switch is packaged now, not a /usr/local stray 2026-07-29 09:14:22 -04:00
Fimeg
8db55cfc27 surfaces: commit the quickshell README, stranded since 07-10
It existed only in archdev's checkout — never added, not gitignored, absent
from the laptop. Two sections had gone stale against the code and are corrected
in the same change: modules/ is no longer "coming", and deploy.sh composes the
whole config rather than swapping Ai.qml.
2026-07-29 08:39:53 -04:00
Fimeg
5965c64781 shell: recover the phone-only edits into the repo
~/souveraine-surfaces/quickshell on the phone is not a git repo and was the
only copy of five files. Brought back verbatim:

- Gestures.qml, SystemGestureRail.qml: TASK-38 osk-swap detent (DUMP §5, §6)
- OnScreenKeyboard.qml: showOsk asks the bus instead of pgrep+sleep 1
- DockAppButton.qml, DockStack.qml: suffix-tolerant AppSearch.resolveEntry

resolveEntry itself had been added to the phone's live ~/.config/quickshell/ii
tree, which deploy.sh rsyncs from ii-base — the next deploy would have deleted
it and left the two dock callers referring to nothing. It lands in ii-base here.
2026-07-29 08:27:27 -04:00
Fimeg
21b602a7d2 ci: repin rust-toolchain to a master commit, not a moved stable tip
29eef336 was the tip of dtolnay's stable branch when pinned. That branch
force-moves each Rust release, orphaning the commit — reachable from no
ref, so act resolves 'reference not found' and rust-test dies before
compiling. That skipped aarch64-artifact and stopped edge publishing
with nothing saying why. master is append-only.
2026-07-28 14:50:52 -04:00
Fimeg
c48737620e sessiond: squeeze is input, and a pocket can veto it
Active Edge becomes InputTrigger::Squeeze. It resets the idle budget
and lands in the trail like any real input, but unlike a power button
it is a sensor reading and a tight pocket is a squeezed chassis, so it
carries the same proximity veto as tap-to-wake (DEVICE-STATE-MACHINE
4).

note_input_gated applies the veto and records the refusal via
record_decision — a silently dropped report is what 10 exists to
prevent. The input op now advertises refused_by_state.

Producers cooperate rather than being enforced; TASK-41 is the gate.
2026-07-28 13:03:53 -04:00
Fimeg
68b6025226 shell: fail closed when the session authority cannot be reached
Every failure path in shellReady answered cb(false) — no lock owed — so a
socket that was not up, a duplicate handshake, or a 3s timeout all concluded
the session was open. The timeout logged 'proceeding without sessiond' and left
the phone unlocked at exactly the moment the authority was unreachable, while
the daemon side of the same contract retakes the lock on heartbeat EOF
'whether or not the session was locked at the time. Fail closed.'

Not knowing the session is locked is not knowing it is not. All of those now
assume locked. Refusals still answer false only for 'already registered', which
means another shell holds the lease and this process is not the authority.

A timed-out handshake is also retried instead of abandoned: a shell that merely
started while sessiond was restarting stayed unregistered for its whole life,
so sessiond saw no heartbeat and raised its fallback surface over ours.

SessionEvents likewise retries session-path resolution rather than disabling
lock-signal monitoring for the life of the process, and logs the gap as an
error while it is open.
2026-07-27 21:38:18 -04:00
Fimeg
deda4a7b6b shell: send locked_ack on re-registration when the surface is already secure
onScreenLockSecureChanged is an edge. After a sessiond restart our lock surface
is already secure, so the edge is in the past and the ack owed by the new
handoff was never sent — sessiond logged 'shell never confirmed its lock after
handoff' and retook the lock 15s later.
2026-07-27 21:30:38 -04:00
Fimeg
4ed8f3dd74 shell: resolve logind session via User.Display, re-register after sessiond restart
SessionEvents asked loginctl for -p ObjectPath, which is not a property — it
returned empty on every boot, so external lock signals were never monitored and
the shell had no unlock ingress. Resolve the graphical session the way
lockhint.rs does; not GetSession(auto) (this shell is a session-less user unit)
and not the /session/auto alias (PropertiesChanged only fires on the concrete
path).

SessiondBridge read root.registered to decide whether to re-register on
reconnect, but the disconnect branch had already cleared it, so the shell never
re-registered after a sessiond restart. Latch it.
2026-07-27 21:21:26 -04:00
Fimeg
45fc69b56a sensord: decay accel motion, debounce light in both directions
Accel inferred moving from orientation edges, so Moving(false) was only
reachable from the startup banner — boot 0 carried 165 true to 8 false and the
keepalive re-sent a stuck true every 30s, pinning +0.3 into observed_confidence
for the life of the process. Motion now decays over MOTION_WINDOW and the
keepalive re-derives it.

Light crossed the lux threshold on one sample and fell back on the next,
pulsing confidence 0.50->0.70->0.50 in 1.2s and toggling promote_idle across
the 0.6 band. An excursion must now persist: 2000ms brightening, 4000ms
darkening, taken from config_autoBrightness*LightDebounce in blueline's vendor
RRO. Reversal restarts the clock.

ParserState is shared and survives monitor-sensor respawns.
2026-07-27 21:21:16 -04:00
Fimeg
3d9340057f sessiond: an expected source that never reports is Absent, not silent
sensord hung off graphical-session.target, which nothing on this device
starts, so it was enabled and dead from every boot. Down starts from a
last-seen stamp and structurally cannot see that. Bind the reporter to
sessiond and make the machine say so.
2026-07-27 15:34:09 -04:00
Fimeg
684d1f9fd5 upower: remove the emptied parents after relocating, not just the subtree
An empty home/casey still ships and still trips the stray check. Runs 1234
and 1248 were both this guard, not the build.
2026-07-27 14:54:04 -04:00
Fimeg
25d1e19246 upower: var/ is a legitimate prefix, not a leak
Run 1234 failed because the new stray check omitted var/lib/upower.
2026-07-27 13:46:48 -04:00
Fimeg
7eb2135906 upower: relocate the cross-install udev leak, and assert it stays gone
udev rules and hwdb installed under the sysroot prefix, so none of the
fork's quirks were ever active on the phone.
2026-07-27 10:11:14 -04:00
Fimeg
2170ba686f deploy: fail loudly when a qmldir names a file the composed tree lacks 2026-07-27 09:15:48 -04:00
Fimeg
53dcb40c5d dial: bind the ring origin instead of assigning it before the surface is sized 2026-07-27 08:29:44 -04:00
Fimeg
002a1a7b08 gestures: consumer + routing table, wired to the dial 2026-07-26 21:01:11 -04:00
Fimeg
bef6e6a653 dial: radial picker, haptic detents, dial IPC 2026-07-26 20:36:07 -04:00
Fimeg
2c1ee8bc00 sessiond: gate the blank actuator on logind LockedHint 2026-07-26 20:16:58 -04:00
Fimeg
29a3c472f5 sensord: buffer every source, and make the light threshold real
Two halves of the same defect. The lux threshold compared each sample to the
previous sample, so a slow drift never tripped it — it now compares to the
last value actually reported. And no source had any rate floor at all, which
is how light managed ~8 reports/sec; 250ms per source, enforced separately so
a busy light sensor cannot delay a proximity edge.
2026-07-26 20:07:31 -04:00
Fimeg
de3a332513 sensord: report edges, not every sample
Light drifts continuously; monitor-sensor emitted ~8 lines/sec and each was
a socket round trip re-asserting the same Changing(false). The keepalive is
what keeps silence meaningful, so only changes need reporting.
2026-07-26 20:03:21 -04:00
Fimeg
be72019883 settings: write the dim value through, drop the invented clamp
Clamping the lock-screen grace to blank-1 turned a 30s dim on a 15s blank
into a 14s grace, dimming one second after you stop touching the phone.
2026-07-26 20:00:50 -04:00
Fimeg
28e8b78284 sensord: needless_bool 2026-07-26 19:16:46 -04:00
Fimeg
43c1fdbc05 session: one IPC owner for target 'session', not a load-order coin flip
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.
2026-07-26 19:04:14 -04:00
Fimeg
83fab1a1b6 sensord: one reporter for every sensor, replacing the per-sensor scripts
blueline-proximity-lock is 113 lines of shell implementing a subtle contract
(heartbeat inside SOURCE_DOWN_AFTER, seed from the probe banner, report both
edges) that would have been hand-copied for light and again for accel. One
binary, one contract, shipped in the package instead of the rootfs overlay.
2026-07-26 18:46:27 -04:00
Fimeg
4d652bbbc6 sessiond: fix the self-deadlock that made the daemon stop answering
Action::Restore bound the brightness inside an if-let, so the MutexGuard
lived for the whole body and run_executor locked again. Non-reentrant: the
state lock was held forever and every later request thread queued behind it
(measured: 347 threads in futex_wait, 691/1024 fds, accept loop healthy, no
request answered in 3h). Bind first, then branch.

Also close connections idle for 120s, exempting the shell heartbeat.
2026-07-26 18:26:31 -04:00