Two gaps in the owner-change handling, both found on device.
The push at adopt time can lose a race. osk-switch sleeps 0.3s before
its deliberate SetVisible(true), and stevia self-shows once it has
finished coming up - both land after the shell has spoken. Measured:
adopt+hide 07:37:51, Visible=true 07:37:52, leaving oskOpen=false with a
keyboard on the screen. Re-assert when the settle window closes so the
last speaker is not the winner.
And the oskSwap gesture never recorded that a swap means the user wants
a keyboard, so osk-switch's re-show and the shell's stale false were
racing with nothing to arbitrate them. Declare the intent first.
sm.puri.OSK0 is a name, not a process. Boot, a crash-restart and an
osk-switch swap each put a fresh keyboard behind it, and the visibility
monitor mirrored the newcomer's opening claim into oskOpen as though the
user had asked for a keyboard. Measured: shell up 03:03:38, self-showed
at 03:03:43, nobody near the phone.
Track the bus owner - gdbus already prints it and the 'Visible' filter
was throwing it away - and on a change push oskOpen at the new owner
instead of pulling state out of it. A process that just started has no
history; the shell is the continuity of intent across the keyboard's
lifetime.
The credential file is shared with `claude` itself and its refresh token is
one-time-use, so whichever process refreshes first rotates the other's copy to
spent. On a failed refresh the provider kept the stale token and 400d every
turn until the service was restarted by hand — measured 2026-08-05, invalid_grant
from 00:03 onward while the file on disk already held a valid pair written at
00:08.
Re-read the file on refresh failure and adopt the token when it is different and
still live. oauth/refresh.rs already does this for the Codex path; the Claude
path never got it.
exclusionMode was ExclusionMode.Ignore, which tells the compositor to
disregard the exclusive zone outright — so the expression below it computed
a height that was thrown away and every window laid out underneath the bar.
The bar is not app space while it is visible.
Auto-hide still yields 0, so a bar configured to hide gives its space back;
the mode only says "honour what I claim".
Not deployed: the phone locked itself on session start, which is
lock-on-startup firing for the first time under viewtop (80cb4b2), and
deploy.sh refuses a live deploy into a locked session.
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.
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.
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.
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.
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.
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.
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.
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.
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".
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.
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.
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.
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.
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.
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.
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.
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.
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.