PAF becomes saf/device (history kept), STATE.md dissolves into saf/state.md with the dated era archived, the substrate SAF moves up from souveraine, and every agreement points at saf/INDEX.md and nowhere else. one map, nothing to remember
10 KiB
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:
scripts/agent/agent-sessions.shinspects recent local records for Souveraine, Claude Code, and Codex and emits one JSON envelope.services/AgentSessions.qmlruns that collector on one cadence, retains the last good envelope, and projects sessions, provider health, active count, staleness, and compact labels.AgentPaneMenu.qmlshows 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:
sessionsis 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;providersreports availability, errors, source-specific limits, and the number of sessions admitted from each source;activecounts records whose observed activity falls inside the active window;tssays 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:
activefrom activity within two minutes;recentfrom activity within one hour;idlefrom 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. 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
AgentSessionsalready 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:
- source — each provider parser is fixture-tested, malformed timestamps do not become plausible epoch-zero state, and provider failure still emits a valid envelope;
- projection — one service retains last-good state, marks staleness, and preserves unknown/windowed/cumulative measurement semantics;
- surface — external sessions render under an explicit observed-only boundary and cannot be selected or controlled;
- installed — the versioned package owns the collector/service/component files used by the running shell;
- live provider — a real session enters and leaves each activity class, and a deliberately unavailable provider is visibly different from an empty one;
- 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.