5.7 KiB
TASK 69 — Agent usage, one spine: the session daemon
Status: recon 2026-08-11, nothing built. Size: staged — see below.
Sources reviewed: knyazin47/codex-usage-quickshell (MIT, upstream clone
for reference), the existing ClaudeUsage in souveraine/surfaces/quickshell/ii-base/,
and souveraine/src/core/session/.
Goal
One collector tracks every agent's activity — Souveraine, Claude Code, and Codex — and feeds the shell. The bar stops owning a fetch per provider; the shell shows projections of one live stream.
What already exists (verified 2026-08-11)
| Source | Truth lives at | Currently read by |
|---|---|---|
| Codex usage + limits | ~/.codex/sessions/**/*.jsonl (event_msg / payload.type == "token_count": total/input/cached/output/reasoning tokens, rate_limits embed, session_meta cwd) |
upstream python collector codex_usage.py |
| Codex live limits | codex app-server --stdio JSON-RPC (initialize → account/rateLimits/read, multi-limit map rateLimitsByLimitId) |
same collector, cached at ~/.cache/codex-usage-quickshell/rate_limits.json |
| Claude usage | OAuth token ~/.claude/.credentials.json → https://api.anthropic.com/api/oauth/usage (5h/7d utilization, per-model, extra credits, resets) |
ClaudeUsage.qml + ClaudeUsageBar.qml in ii-base |
| Claude local sessions | ~/.claude/projects/<slug>/*.jsonl — per-assistant usage with input/output/cache_creation/cache_read + model + timestamp |
nothing — gap |
| Souveraine sessions | core/session/mod.rs Session/ConversationMessage with TokenUsage; assistant_with_usage has zero callers (TASK-67) |
nothing — gap |
The two gaps are the same shape: the substrate already records the truth, and nothing consumes it. The daemon is the consumer that ends TASK-67's blindness as a side effect.
Staged build
- Collector daemon (Rust, in
souveraine): one unix socket ($XDG_RUNTIME_DIR/souveraine-sessions.sock), newline-delimited JSON, the wire shape already proven by the island'sAgentService.qml(TASK-70): per-provider session table, urgency-ranked list, pending permission queue, IPC verbs. Providers: Codex (port the collector's JSONL parse + app-server probe), Claude (local JSONL + OAuth usage poll), Souveraine (substrate session files). - Envelope — one JSON shape for all three: window label, used percent, resets, tokens today/week/last turn, activity series. The upstream collector's output is already close; don't invent a second vocabulary.
- Shell projections —
ClaudeUsage.qmland the Codex panel become thin reads of the daemon socket. Delete the per-provider timers/processes from QML; one daemon owns cadence. - Limits merge — Codex live + cache + local fallback ladder already exists upstream; Claude's OAuth poll has a retry ladder. Keep both, from one owner.
Acceptance
A bar showing both providers from one socket. A killed daemon degrades to
"unavailable", never blanks the bar. The Souveraine substrate's own
assistant_with_usage counters appear on the same panel — TASK-67 gets its
surface.
Connects to
TASK-70 (the island — the daemon is its data source), TASK-67 (usage counters
finally read), ClaudeUsageBar.qml + codex_usage.py (the code being
absorbed), core/session/mod.rs (canonical session record).
Stage 1 landed 2026-08-11 — the envelope exists and is real
Commits 47ce2d1, b211842 in souveraine@primary; activating edits queued as
surfaces/quickshell/patches/0005.
scripts/agent/agent-sessions.sh emits the envelope described above, from
all three sources, in 0.47 s. Both gaps in the table are closed: Claude local
JSONL and Souveraine conversations are read for the first time.
The envelope is the contract; the transport is not. The collector is a
polled script, not the Rust daemon. That is deliberate — it produces the exact
shape the daemon will serve over
$XDG_RUNTIME_DIR/souveraine-sessions.sock, so the surface can be built and
proven against real data now, and the daemon swap changes one line in
AgentSessions.qml and nothing downstream. Do not let surface code reach past
the service's properties into the JSON.
Contract held deliberately: the collector always exits 0 and always emits
one line. A provider that fails reports available:false with a reason rather
than taking the envelope down. A blank bar is a worse failure than a stale one,
so AgentSessions retains last-good data and raises stale instead of
emptying.
Two bugs found by running it, not reading it
- jq's
fromdateiso8601accepts only%Y-%m-%dT%H:%M:%SZ. All three providers emit fractional seconds (Claude.179Z, Codex.238Z, Souveraine.126950960Z), so every timestamp parse failed — and a baretry/catchturned that into epoch 0, which rendered as "idle" for sessions that were live that second. The failure looked exactly like a correct answer. Fixed by stripping the fraction, and a parse miss now yields -1 so a future breakage is visible rather than plausible. - The Codex session id was the rollout date prefix, not the uuid.
TASK-67's blindness is now visible instead of theoretical
Souveraine sessions report tokensIn/Out: -1, not 0, because the substrate
records TokenUsage but assistant_with_usage still has zero callers. -1 is an
admission; 0 would be a measurement claim we cannot back. Surfaces render it as
"—". The day TASK-67 lands, real numbers appear and nothing else changes.
Still stage 1
- ClaudeUsage keeps its own OAuth poll. Stage 3 folds it in; until then
AgentSessions.codexLimitsis honest about covering only what the collector sees. - Nothing renders yet. See TASK-70.