Watch
1
0
Fork
You've already forked souveraine
0

saf: give the panel's next cuts an owner

This commit is contained in:
Fimeg 2026-08-17 12:04:49 -04:00
commit 184a449731
7 changed files with 441 additions and 12 deletions

View file

@ -24,6 +24,8 @@ How Souveraine is built, and why. For whoever reads it, human or agent. When thi
- [session start](surface/02-session-start.md). One package-owned dependency graph from compositor socket to authority to shell. - [session start](surface/02-session-start.md). One package-owned dependency graph from compositor socket to authority to shell.
- [USB Hands](surface/03-usb-hands.md). An armed wire, an optional trackpad, and one acknowledged hand into the attached host. - [USB Hands](surface/03-usb-hands.md). An armed wire, an optional trackpad, and one acknowledged hand into the attached host.
- [agent panel](surface/04-agent-pane.md). One Souveraine conversation, typed thought and action, and truthful controls at the hand. - [agent panel](surface/04-agent-pane.md). One Souveraine conversation, typed thought and action, and truthful controls at the hand.
- [agent presence](surface/05-agent-presence.md). One observed session projection, kept rigorously separate from control and permission.
- [surface lifetimes](surface/06-surface-lifetimes.md). A small resident shell, optional workers that truly leave memory, and proof before a rewrite.
Coming: the rest of identity (the memory filesystem, the ledgers), the nervous system (schedules, the event log), the consciousness cycle, the runtime, federation, a glossary. Coming: the rest of identity (the memory filesystem, the ledgers), the nervous system (schedules, the event log), the consciousness cycle, the runtime, federation, a glossary.

View file

@ -52,6 +52,10 @@ accept selection and never imply that the composer can steer them. Observation
and control can share a menu only when the boundary is visible in words and in and control can share a menu only when the boundary is visible in words and in
behavior. behavior.
[Agent presence](05-agent-presence.md) owns the collector contract, provider
health, activity heuristics, and the future path toward one session service.
This chapter owns only how that projection enters the conversation surface.
The Panel eventually needs richer active-agent status: substrate reachability, The Panel eventually needs richer active-agent status: substrate reachability,
conversation attachment, primary/subconscious phase, queued interjections, and conversation attachment, primary/subconscious phase, queued interjections, and
the last terminal event. Those facts belong to the Souveraine connection, not the last terminal event. Those facts belong to the Souveraine connection, not
@ -178,6 +182,10 @@ geometry. Souveraine-owned `AgentMessage`, `ThinkingCard`, `ToolCard`,
it. The next structural cut is an owned Panel host and conversation viewport it. The next structural cut is an owned Panel host and conversation viewport
that consume the same service without copying the adapter's state. that consume the same service without copying the adapter's state.
Moving the Panel out of the resident shell is a separate lifetime boundary,
not a reason to fork its conversation model. [Surface lifetimes](06-surface-lifetimes.md)
owns that extraction and its memory/fault proof.
That cut should be rehearsed beside the live pane, then swapped once. A QML That cut should be rehearsed beside the live pane, then swapped once. A QML
write-reload loop over the only working chat surface turns each syntax fault write-reload loop over the only working chat surface turns each syntax fault
into a session-wide interruption and can abandon a live turn. Candidate and into a session-wide interruption and can abandon a live turn. Candidate and

View file

@ -0,0 +1,224 @@
# Agent presence
Agent presence answers one narrow question: **which agent sessions can this
machine currently observe?** It does not decide which agent a conversation
controls, approve a tool on another process's behalf, or turn a terminal
session into a model behind the Panel.
That distinction matters most where the UI is smallest. A Codex process can be
running beside Souveraine while the Panel composer still addresses only the
selected Souveraine agent. Showing both is useful. Making them look equally
selectable is a lie.
## Three different facts
Keep these independent even when one menu draws them together:
- **controlled agent** — the Souveraine agent selected through `/v1/agents`;
the Panel can create or resume its conversations and send it a turn;
- **observed session** — a recent Souveraine, Claude Code, or Codex record found
by the local collector; it may be active, recent, or idle, but observation
grants no action;
- **provider health** — whether a provider's source was actually inspected.
Unavailable is not the same as available with no sessions.
The controlled agent is a server relationship. Observed activity is a local
projection. Provider health is evidence about that projection. Do not infer
one from another.
## What exists now
The current path is deliberately small and transitional:
1. `scripts/agent/agent-sessions.sh` inspects recent local records for
Souveraine, Claude Code, and Codex and emits one JSON envelope.
2. `services/AgentSessions.qml` runs that collector on one cadence, retains the
last good envelope, and projects sessions, provider health, active count,
staleness, and compact labels.
3. `AgentPaneMenu.qml` shows Claude Code and Codex sessions beneath
**Running elsewhere · observed only**. Those rows are inert.
The shell also retains `modules/souveraine/island/Island.qml` and
`IslandExpansion.qml`, written against the same service. They are not mounted
in the current bar or Panel. They are reusable observer components, not a
second live presence surface and not a reason to put agent chrome back above
the conversation.
They are also not abandoned. Their next proper home is the phone's later
dynamic top-bar system, once the bar can host more than a fixed row of widgets.
The present Panel move lets that larger work wait without making the Island
components disposable.
This is source and current composed-surface behavior. It is not the proposed
Rust session daemon. The collector still starts a shell process from QML on
each poll.
## The envelope contract
Transport may change; these meanings must not:
- `sessions` is newest first and contains provider identity, stable session
identity, a useful label source, last activity, activity state, and only the
usage fields the provider actually supplied;
- `providers` reports availability, errors, source-specific limits, and the
number of sessions admitted from each source;
- `active` counts records whose observed activity falls inside the active
window;
- `ts` says when the envelope was collected, not when every underlying source
was updated;
- a failed provider degrades inside the envelope instead of aborting the other
providers;
- a failed whole refresh keeps the last good model and marks it stale instead
of replacing it with an authoritative-looking empty list.
Unknown is a real value. The current shell uses `-1` for an unavailable token
measurement and renders an em dash, never zero. Souveraine now persists
per-turn usage with assistant messages, but `conversation.json` does not expose
an aggregate for the collector, so the presence projection still reports
Souveraine session usage as unknown. That projection may be improved; the UI
must not manufacture a total in the meantime.
Claude's local usage is read from a bounded recent tail and is labelled as
windowed. Codex's latest token-count record is cumulative for that rollout.
Those numbers are not interchangeable merely because both fit in one field.
Any combined view must retain the source semantics or omit the comparison.
## Activity is a heuristic, not cognition
The present collector derives:
- `active` from activity within two minutes;
- `recent` from activity within one hour;
- `idle` from older admitted records.
These are filesystem/event-age classes. They do not prove that an inference is
running, a tool is still alive, an agent is waiting for Casey, or a response is
complete. In particular, there is no synthetic `waiting` state. Waiting for a
permission decision requires a real request lifecycle; timestamps cannot stand
in for it.
The Panel's own active-turn, primary/subconscious phase, cancellation, and
terminal event must continue to come from the Souveraine stream described in
[the agent panel](04-agent-pane.md). Presence may corroborate that state. It
must not overwrite it.
## Where presence belongs
The close conversation surface puts observed external sessions inside the
footer's agent menu because that is where mistaken control is easiest to
prevent: selectable Souveraine agents first, an explicit divider, then inert
external rows.
A bar, island, overview, or diagnostics page may project the same service when
the device has room and the projection answers a real glance question. On a
phone, permanent agent-session chrome competes with system state and was
rejected on glass. Absence from the bar is therefore a product decision, not a
collection failure.
No surface may:
- label a discovered process as controllable without an admitted action path;
- show unavailable providers as zero sessions;
- imply that recency means work is still running;
- expose conversation text, prompts, repository paths, or tool payloads merely
to make presence richer;
- start its own provider poll when `AgentSessions` already owns the projection.
## The later dynamic bar
The phone's top bar should ultimately be an extensible host whose components
can enter, compact, expand, yield, and leave according to live state. Agent
presence is one such component, beside notifications, media, connectivity,
power, privacy, and other device facts. A collection of independent widgets
all changing width is not that system.
Before the Island is mounted again, the bar needs a shared contract for:
- component identity, priority, disclosure class, and device eligibility;
- resting, compact, attention, expanded, and hidden forms;
- one arbiter for horizontal space and collisions;
- preemption and return when a higher-priority event arrives;
- dwell, decay, and animation timing so rapid events do not thrash geometry;
- tap, long-press, dismiss, and expansion ownership;
- lock-state redaction and a safe minimal form;
- a stable overflow or secondary surface when several components deserve
attention at once;
- component failure that removes one projection without taking down the bar.
`Island.qml` and `IslandExpansion.qml` are useful first clients for that host:
they already separate a compact morph from expanded detail and consume the
shared presence projection. They do not yet define the host protocol, arbitrate
against neighbours, or prove the phone form on glass. The dynamic bar is a
larger surface task carried here until it has enough implementation scope to
need its own SAF chapter; the old fixed bar placement is not its prototype by
default.
## Permission is a different authority
The earlier island exploration considered Claude hook installation and
Allow/Deny controls. None of that is present in Souveraine's current path. The
collector does not edit `~/.claude/settings.json`; there is no permission
bridge; observer rows do not jump to or command external terminals.
If permission control is built later, it needs more than an attractive card:
- the request must name the exact provider session, tool, arguments, and age;
- the producer and target must be authenticated strongly enough for the
consequence of the action;
- allow-once, deny, expiry, cancellation, and duplicate delivery need explicit
states;
- the decision and its delivery result must enter the audit trail;
- provider failure must fall back to that provider's normal safe behavior;
- a broad bypass or allow-all posture requires a separately admitted policy,
not a more convenient button.
Until that path is owned and attested, presence stays read-only.
## The intended service
One long-lived session service remains the destination. It should replace the
poll subprocess, not change the envelope under every surface. The proposed
Unix socket at `$XDG_RUNTIME_DIR/souveraine-sessions.sock` is a candidate
transport, not proof that the daemon exists.
The service needs:
- one cadence and one adapter per provider;
- event-driven updates where a provider supports them, with bounded scans as a
fallback;
- provider-specific provenance and measurement semantics preserved in the
normalized record;
- last-good/stale behavior at both provider and whole-envelope level;
- stable session identity across refreshes;
- bounded resource use and no transcript-content collection merely for a
presence label;
- an authenticated, separate action channel if external control is ever
admitted.
Souveraine's own session and usage facts should eventually be projected from
the substrate directly rather than rediscovered through files written for
persistence. Claude and Codex adapters remain local observers unless their
providers expose a stronger owned event path.
## Proof boundary
Presence work is proven in layers:
1. **source** — each provider parser is fixture-tested, malformed timestamps do
not become plausible epoch-zero state, and provider failure still emits a
valid envelope;
2. **projection** — one service retains last-good state, marks staleness, and
preserves unknown/windowed/cumulative measurement semantics;
3. **surface** — external sessions render under an explicit observed-only
boundary and cannot be selected or controlled;
4. **installed** — the versioned package owns the collector/service/component
files used by the running shell;
5. **live provider** — a real session enters and leaves each activity class,
and a deliberately unavailable provider is visibly different from an empty
one;
6. **authority**, if ever added — an attested decision reaches only the named
request and is recorded end to end.
The current implementation has the polling envelope and Panel projection. The
Rust daemon, aggregate Souveraine usage projection, provider permission path,
and cross-provider control are not yet true.

View file

@ -0,0 +1,194 @@
# Surface lifetimes
The shell needs a memory budget and failure boundaries, not a superstition
about which UI language is pure. Permanent state, optional presentation, and
authority must have named owners and observable lifetimes. If closing a heavy
surface leaves its objects and pages resident in the only process holding the
lock screen, the surface did not meaningfully go away.
## Evidence that opened this question
The daily-driver phone has 3.5 GB RAM. Three measurements from August 2026
frame the risk without pretending to be a fresh baseline:
- QuickShell reached about 610 MB RSS during the Live2D experiment;
- the summoned WebKit face added about 283 MB RSS in its own process;
- a separate dynamic-QML/per-stream rendering storm drove QuickShell to about
952 MB anonymous RSS before the kernel killed it.
These observations do not prove that ordinary QML costs 952 MB, that WebKit is
always too expensive, or that a Rust surface is automatically small. They do
prove that an optional renderer can consume a material fraction of the phone's
memory and that a storm inside the resident QuickShell process shares a
failure domain with essential session UI.
The numbers must be remeasured against exact package revisions and process
trees before they become a budget.
## The resident core
The resident shell keeps only surfaces and connections that must survive the
loss of optional presentation:
- sessiond heartbeat and the secure session-lock handoff;
- the lock surface and lock-tier disclosure gate;
- display, idle, and device-state projections needed to remain safe;
- the small gesture and power paths required to recover or close a surface;
- a minimal system-status/recovery indication.
The resident core is not a second authority. Sessiond remains the decision
owner for lock, power, and admitted device action; the shell renders its state
and carries named requests. [Session start](02-session-start.md) owns the
package and systemd graph that brings compositor, authority, and shell up in
order.
The secure lock path must not be conditionally absent while a shell reload is
in flight. Optional-surface work may not reopen the already-known race where a
new shell requests a lock before the old holder has transferred or released
it.
## Optional means separately disposable
The agent Panel, Live2D presence, web content, large explorers, and future
special renderers are optional surfaces. Each needs:
- a named package and executable or explicit in-process owner;
- one startup trigger and one authority that may request presentation;
- a shutdown rule after close, idle, lock transition, or resource pressure;
- a state restoration key whose authority lives outside the renderer;
- a disclosure class and locked-state behavior;
- a resource line measured while closed, opening, active, and closing;
- a supervisor policy and a visible failure event.
A QML `Loader` is useful for avoiding construction and destroying object
graphs. It is not proof that allocator pages returned to the operating system,
and it cannot stop a rendering storm from killing the process that owns it.
Call that path lazy only after object lifetime is verified; call memory
released only after the OS measurement falls.
For expensive surfaces, a supervised process is the preferred boundary. When
the process exits, its private memory and faults leave with it. The resident
shell may retain a small launch/status projection, never the heavy renderer's
state graph.
## The Panel is the first extraction
The agent Panel is the strongest first worker candidate because its authority
already lives elsewhere:
- the Souveraine server owns agents, conversations, messages, itinerary,
cancellation, interjection, and replay;
- sessiond owns session/lock/device authority;
- the Panel owns presentation, input collection, transient selection, and
viewport state.
An extracted `souveraine-agent-surface` must therefore consume the same typed
server contract described in [the agent panel](04-agent-pane.md). It must not
copy conversations into a new local authority or become a second writer during
candidate testing.
Its lifecycle should be explicit:
1. an unlocked, admitted presentation request launches the version selected by
the installed surface package;
2. the worker authenticates to the local Souveraine endpoint and attaches only
after Casey selects or explicitly resumes a conversation;
3. close detaches presentation and gives the worker a short bounded grace for
non-authoritative UI state;
4. the supervisor ends the worker after that grace;
5. reopen reconstructs from substrate truth and may restore harmless viewport
state separately;
6. lock immediately hides Personal-class content and follows the declared
stop/blank policy even if the worker is stuck.
During an active stream, a killed worker must not cancel the server turn unless
Casey issued cancel. The substrate continues or records its terminal state;
reopening reattaches and backfills. Conversely, a worker must not remain alive
forever merely because a server stream forgot to close. Presentation lifetime
and inference lifetime are related state machines, not one process lifetime.
## Fault containment
The resident shell needs a small supervisor projection for each optional
surface: stopped, starting, ready, active, stopping, failed, and backoff. A
crash becomes an event with the package version and last lifecycle edge. It
must not silently relaunch in a tight loop or take the session lock with it.
The decisive fault tests are physical:
- kill the Panel during streamed text and during a large tool return;
- feed a candidate QML version that fails to load;
- wedge the worker so it ignores close;
- lock while Personal-class content is visible;
- exhaust the worker's memory budget without exhausting the resident core.
In every case the lock surface, power path, sessiond heartbeat, and recovery
control must remain available. A screenshot after restart is not fault proof.
## Versioned delivery on every body
The laptop and phone receive published packages from Gitea. A surface split is
complete only when the package graph installs compatible versions and the
systemd/session start path selects them consistently. Hand-placing a worker on
one device proves a diagnosis at most; it is not the fix future installs will
receive.
The core and optional-surface protocol must be versioned. Compatibility needs
an explicit range or handshake so a newer Panel cannot silently interpret an
older substrate event shape. Package upgrade stages a candidate; adoption and
restart remain explicit where a live conversation or lock handoff could be
disturbed. The last working surface stays available until the candidate reaches
ready state.
Gitea Actions and ArchDev own compilation and package artifacts. Local checks
may parse QML, format Rust, validate manifests, and exercise fixtures; they do
not replace the package result.
## Native Rust is a measured option
A Rust Wayland client could replace some or all of QuickShell, but it still has
to own layer-shell/session-lock protocols, rendering, text, input, animation,
accessibility, service integration, and recovery. Rust does not remove the
need for resident/optional process boundaries, and a native screenshot says
nothing about steady memory or fault behavior.
The useful spike is deliberately small: one native layer-shell card, one typed
sessiond-fed state, one package, and measurements for RSS/PSS, launch latency,
and failure. It does not take the lock, replace the shell, or grow a parallel
settings authority. Compare it against the extracted QuickShell worker under
the same workload before choosing a rewrite.
## Measurement record
Every budget line should name:
- device and available RAM;
- source commit, package version, and protocol version;
- process tree and which surfaces are constructed;
- RSS and PSS after a defined settle time;
- peak during a defined action, including a representative streamed tool turn;
- memory after close and after worker exit;
- launch-to-ready latency;
- result of the relevant kill/lock/reopen test.
Measure at least resident idle, Panel closed, Panel open and idle, active text
stream, large tool payload, Live2D closed/open, and post-close. A single `ps`
line without surface state is anecdote, not a budget.
## Proof and present boundary
The path is complete only when:
1. the resident and optional sets are explicit in source and packages;
2. closing the Panel ends its worker and the OS-visible memory leaves;
3. killing or wedging that worker leaves lock, heartbeat, power, and recovery
alive;
4. locked-state disclosure is exercised, not inferred from a conditional;
5. reopen restores the authoritative conversation without duplicating a turn;
6. Gitea emits the versioned core and surface packages used by both laptop and
phone;
7. a Rust rewrite decision, if made, cites comparable measurements.
None of that extraction is true yet. The current Panel remains inside the
resident QuickShell process; the August numbers are historical observations;
and the Rust path remains a bounded experiment, not an adopted shell plan.

View file

@ -132,9 +132,9 @@ Rectangle {
} }
// "" where a provider does not report tokens. Rendering a // "" where a provider does not report tokens. Rendering a
// 0 would be a measurement claim we cannot back: the // 0 would be a measurement claim we cannot back. The
// Souveraine substrate records TokenUsage but nothing calls // substrate persists per-turn usage, but this presence
// assistant_with_usage yet (TASK-67). An em dash is the // projection does not aggregate it. An em dash is the
// honest glyph for "not measured". // honest glyph for "not measured".
StyledText { StyledText {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter

View file

@ -136,11 +136,12 @@ codex_json() {
# ------------------------------------------------------------ souveraine ---- # ------------------------------------------------------------ souveraine ----
# ~/.souveraine/server/agents/<agent>/conversations/<conv>/conversation.json # ~/.souveraine/server/agents/<agent>/conversations/<conv>/conversation.json
# carries updated_at + message_count. It does NOT carry token usage: the # carries updated_at + message_count. It does NOT carry aggregate token usage:
# substrate records `TokenUsage` but `assistant_with_usage` has zero callers # the substrate persists per-turn `TokenUsage` in its message record, while
# (TASK-67). We report -1 for tokens rather than 0, because 0 is a measurement # this bounded presence scan intentionally reads only conversation metadata.
# and -1 is an admission. The day TASK-67 lands, this starts reporting real # We report -1 rather than 0, because 0 is a measurement and -1 is an
# numbers and nothing else has to change. # admission. A direct substrate projection can fill the fields later without
# changing the surface contract.
souveraine_json() { souveraine_json() {
local dir="$HOME/.souveraine/server/agents" local dir="$HOME/.souveraine/server/agents"
[ -d "$dir" ] || { echo '{"available":false,"error":"no ~/.souveraine"}'; return; } [ -d "$dir" ] || { echo '{"available":false,"error":"no ~/.souveraine"}'; return; }

View file

@ -113,10 +113,10 @@ Singleton {
} }
// Total tokens for a session, or -1 when the provider genuinely does not // Total tokens for a session, or -1 when the provider genuinely does not
// report them. -1 is deliberate: the Souveraine substrate records TokenUsage // report them. -1 is deliberate: Souveraine persists per-turn TokenUsage,
// but `assistant_with_usage` has zero callers (TASK-67), so a 0 here would // but the conversation.json metadata inspected by the presence collector
// be a measurement claim we cannot back. Surfaces must render -1 as "", // does not aggregate it. A 0 here would be a measurement claim we cannot
// never as zero. // back. Surfaces must render -1 as "", never as zero.
function sessionTokens(s) { function sessionTokens(s) {
if (!s) if (!s)
return -1; return -1;