TASK-72 step 2. AgentMessage consumes the typed segment list and dispatches
to owned primitives; ThinkingCard draws reasoning as a segment kind rather
than as <think> punctuation inside a flattened string, which removes the
think-fence collision class by construction.
MessageTextBlock and MessageCodeBlock are deliberately reused from the vendor
snapshot: they are markdown renderers, not agent vocabulary.
Composed by MANIFEST, imported by nothing. The activating edit is queued as
patch 0008. Qt6 qmllint: zero syntax findings; remaining warnings are the
widget-resolution class the vendor AiMessage also produces (38 vs 46).
Not verified: nothing has loaded this. No real tool round has rendered here.
First step of TASK-72's owned destination. modules/souveraine/agent/ is
original Souveraine code; no further agent-surface work lands in ii-base.
ToolVocabulary transcribes src/ui/chat/tool_renderers.rs — the same event
stream, solved once already for the TUI. All 19 registered tools have a kind,
an icon and a summary. The vendor card knew 8: bash read write edit grep glob
list_dir memory. The 11 it could not name are the interiority verbs.
ToolCard groups by what the act is rather than which subsystem serves it, so
acts on the machine and acts on herself read as different families. Status is
legible without opening the payload; collapsed by default except running or
failed.
Composed by the manifest, imported by nothing. Registration is a later step.
Authored in a parallel session (Rook, Codex side) on 2026-08-12; committed
here because untracked is the fragile state, not because authorship is mine.
Finding stands on its own: StepUpAuth called souveraine-pam-auth, a binary
never written, and fell back to pkcheck against an action never shipped, so
every grant request was silently denied. Unapplied, unloaded.
BackendEvent::ContextPressure was (f32, usize) whose second element was the
context limit. A positional tuple crossing a module boundary made every
consumer guess: the TUI guessed limit and was right, the HTTP layer named it
tokens and was wrong. So every non-TUI surface rendered the ceiling as the
usage — a constant 250000 that looked like a measurement. Nothing failed and
nothing logged.
Name the fields, and carry tokens_used explicitly rather than leaving it to
be reconstructed as pressure x limit. bifrost_pressure already computed the
token count and discarded it.
blueline-fingerprintd installs /usr/lib/security/pam_souveraine_fpc.so
(PKGBUILD:48). cc541d1 named pam_fpc_polkit.so, which exists nowhere.
The leading '-' means a missing module is skipped without complaint, so
a wrong name here fails silently and permanently: the finger does
nothing and no log says why.
The module returns PAM_IGNORE outside PAM_SERVICE=polkit-1
(fpc-polkit-pam.c:86), so step-up falls through to the password stack
until that allowlist widens.
SESSION-AUTHORITY-DOCTRINE §3 names /etc/pam.d/souveraine-stepup by path.
Root-owned config can only arrive by package: rootfs-overlay/ reaches a
device on flash and the phone does not flash.
auth_self, never auth_admin — step-up re-proves the user to themselves, it
does not escalate. The leading '-' on pam_fpc_polkit.so loads the fingerprint
factor only where it is installed, so one file serves phone and laptop.
Inert until a consumer calls it; no src/ path does yet.
529 is Anthropic's own overload signal on this wire, so classify_status
treated it as Permanent and bailed the whole request with zero retries.
The bail string reaches the surface, where it reads as an auth failure.
Adds 529 to the transient arm and a status_hint() clause naming capacity
rather than credentials, on both the retry warning and the final bail.
Null-valued options serialised into config.json and segfaulted
JsonAdapter on the next launch, killing the shell after load.
Tristate strings (auto/on/off) with boolean fallback.
The island had never been load-tested. All three were fatal or noisy and
none were visible by reading.
StyledToolTip takes `text`, not `content`. Two occurrences, each one a
hard "Cannot assign to non-existent property" that failed the entire
module chain up to shell.qml — the bar would not have come back.
The provider-health tooltip could never have shown even once fixed: a
MaterialSymbol has no `hovered`, and our own StyledToolTip fix treats a
non-hoverable parent as never-show. That block exists specifically so an
unavailable provider is not mistaken for "no sessions", so a tooltip was
the wrong carrier. Stated visibly now.
Island read root.QsWindow.window through a non-optional access; inside a
Loader the attached object is not resolved at construction and it threw.
Revealer takes implicitHeight from childrenRect, so anchoring its child
to its own centre closes a cycle once it sits in a Loader. ii-base hid it
by letting the layout drive the Revealer height directly.
Verified: qs -c souveraine reaches "Configuration Loaded" with no warning
naming any of these files.
ii-base and ii-phone each carried a 357-line BarContent to express eight
differences, all of them either "is this widget shown" or "which slot is
it in". No behaviour differed. Every bar edit had to be made twice or
diverge silently against the pin.
Widgets are now declared once as Components in a registry; each slot is a
Repeater over a list of widget names, so placement and order are data.
Config names a slot or a device profile picks it. "auto" derives the
profile from the same cramped-ness test behind useShortenedForm, so the
phone keeps its arrangement with no config file. Loaders are active only
when placed, so an unplaced widget is never constructed.
The agent island becomes a placeable widget rather than a mount smuggled
into UtilButtons: it reaches every device bar from one declaration.
ClockWidget is unified for the same reason. Its phone fork existed solely
to add customFormat, which BarContent now sets on every device.
bar.agentSessions was landing under background.widgets, not bar, so
Config.options.bar.agentSessions was always undefined and only the ?? true
fallback made the island appear wired. Moved, and merged into the existing
bar.indicators rather than declaring a second one.
Collector emits a single JSON envelope for every agent session (Souveraine,
Claude Code, Codex) and always exits 0; a failing provider reports
available:false rather than taking the envelope down.
AgentSessions.qml owns the cadence and projects the envelope. Transport is not
the contract: when the TASK-69 daemon lands, only the source changes.
Island is a morph host for the bar - dot on a cramped bar, pill where there is
room, derived from the same threshold BarContent uses.
Inert until registered in services/qmldir and mounted; see patches/0005.
One unreadable conversation.json aborted the whole listing, so hydration
returned an error, the caller read it as no prior thread, and a fresh
conversation opened on every boot. Skip the damaged record and warn.
Pick the thread by activity, not registration order: list_for_agent().last()
is the oldest conversation once hydration has pushed them newest-first.
Same fix for the scheduled background turn.
`Message.content` was `String`, so an attachment could not reach the
substrate even though everything downstream was ready for it: the session
already stores `ContentBlock::Image`, `run_turn` already builds OpenAI
multi-part content, and the provider client already speaks `image_url`.
The whole pipe was plumbed and the tap was welded shut at the door.
`MessageContent` is untagged — a bare JSON string is exactly the wire it
has always been, an array carries ordered parts in the substrate's own
`ContentBlock` vocabulary. Same shape `ContentValue` uses facing the
provider, turned inward. No new noun.
Only `text` and `image` cross. `tool_use`, `tool_result` and `reasoning`
are the engine's to write; a surface posting one gets a 400 rather than a
forged turn quietly filed in history. And the vision gate now answers at
the door: an image sent to a text-only model returns 422 naming the model
instead of being stripped to "[Image: attached by user]" downstream,
which reads to the human like she looked and said nothing. Conversion
happens for every message before any of them is stored, so a rejection in
the second cannot leave the first half-committed.
Nine tests: legacy string, typed text, mixed order, round trip, replay
out of persistence, refusal of engine-owned blocks, the gate's question.
The surface half is untouched. `Ai.qml::attachFile()` still refuses, and
its comment is now out of date — that is the next checkpoint, and it is
Casey's to compose and restart.
/home/casey/souveraine on the build box is a full copy of this repo with no
.git at all, newest sources 2026-08-09 21:07. Audited by hashing every file
and querying this repo's object database: 1545 checked, ten unknown after
excluding the vendored upower submodule.
Eight carry real deltas; roughly fifty substantive lines have no upstream
equivalent. They are the follow-up to b2774c8 that was never committed: a
logind capability query before firing a power verb, and a failure-path release
of the in-flight power latch without which a refused suspend deafens the verb
until the next boot.
Preserved verbatim, not merged. The tree has moved 100+ commits and the
sessiond power seam wants a human.
write replaces a whole file and reported success identically whether the
file grew, held, or was erased. Replacing an existing file now requires
overwrite: true, and the refusal names append/edit as the likely intent.
Every write returns a WriteDelta so the result carries magnitude; a write
that drops a file below half its former size says so loudly.
Internal callers (archivist, compaction, subconscious ledgers, REST) keep
the ungated path — their replacement is structural.
Keep assistant, reasoning, tool-call, and tool-return frames structured through the sidebar delegate. Tool cards bind returns by wire id and fold their output by default.
Reasoned from the SSE contract and existing TUI renderer. Diff check passed. Untested against a running shell: Casey applies and reloads staged patches.
A tool_call/tool_return arriving while a <think> block was open emitted its
own self-closing fence, whose </think> closed the outer block early. Every
reasoning token after that point rendered as prose, and the stray close left
the trailing assistant text mis-segmented. Append inside the open fence
instead; only open one when arriving in prose.
The composed config is symlinked into the repo and quickshell reloads on
write, so an agent editing QML edits the live shell mid-turn. Stage changes
as patches instead; the human applies and reloads on their own beat.
First in the queue: 0001-resume-offer — the agent-established hook offers
the latest thread instead of silently attaching it. Explicit resume paths
and the e4e6594 amnesia fix are untouched. Untested against a running
shell.
The Err branch surfaced the same 'no anomalies detected' text as the
success path, at Low urgency. A subconscious erroring on every turn was
indistinguishable from one noticing nothing. The only record of the real
cause was a tracing::warn on a stderr whose socket has no reader.
Carry the error in-band at High, and say the pass failed.