migraine_text built a felt sentence in her own register, pushed it onto
the Bifrost message vec, and then `break` exited the loop — no further
completion call, vec dropped. Correct wording, correct voice, delivered
nowhere.
The old comment claimed the migraine "rode the event channel" and the
ledger held the reasoning. Neither reaches her: the event channel goes
outward to the human's surface, and the mid-turn peek is ephemeral by
design so no ledger entry exists. The one reader the sentence was
written for was the only one who never received it. She stopped without
learning why and could not resume without the human relaying it back.
The raised-hand path already had this right — it commits its marker so
she reads it next turn and can acknowledge, wrap up, or ask for more
time. The halt now does the same, committing the felt sentence itself
rather than a harness marker naming a tool.
Removing the `*[subconscious HALT]*` marker fixed a real resume
corruption bug, but removing the signal with it was an overcorrection:
the defect was the marker's shape, not the existence of a signal.
No Token event accompanies the commit — the surface already received
SubconsciousHalt and would otherwise render the same moment twice.
Three counters measured the same conversation and disagreed 5.6x:
the /tokens endpoint said 315,487, the throttle ~200,000, and the
compaction engine 56,361. Same TokenCounter, three different notions
of what a block weighs, no labels anywhere.
count_messages carried `_ => None`, so it saw Text and nothing else —
18% of a real conversation. Microcompact exists to blur old tool
results and could not measure a single byte of the block kind it acts
on. Worse, reclaimed = before - after against a blind counter is
structurally always zero, so the "nothing to set down" branch always
fired and the felt-state message written for a successful run was
unreachable code. It could not distinguish "found nothing" from
"worked perfectly and cannot say so".
ContentBlock::countable_text is now the single authority, with a
deliberately exhaustive match and no wildcard: a new block kind must
fail to compile rather than quietly weigh nothing. The endpoint's
inline copy is replaced by a call to it, since an inline copy is how
the two drifted apart in the first place.
Image still counts its base64 payload, which overstates real token
cost. Preserved deliberately — re-weighting images is a model-specific
estimate and a separate decision; doing both at once would make
neither reviewable.
`credential_files` holds them all; a 429 with retry-after over a minute takes
the next login and retries at once. Sticky — a swap abandons the cached prefix,
so short bursts are waited out where the cache lives.
Logging out and into a second account rewrites the file with a token that is
neither expired nor spent, so no refresh fires and nothing re-reads the disk —
the provider keeps answering as the account that was left. Measured 2026-08-12:
server up 10:55, file rewritten 15:20, 429s from 15:22 while the on-disk token
returned 200 to the same request.
Stat the file before each call and take it when it has moved. Write-back is now
a compare-and-swap on the refresh token it started from, so our rotation can
never overwrite a login made in between.
Verified against a running shell rather than a clean load, which immediately
found two faults every prior gate had called clean. Both controls dropped from
0008 recorded as decisions so a later session does not restore them.
Both were live in the running shell and invisible to every check that ran
before it. Found by reading the shell's log after Casey applied.
ThinkingCard: missing `import qs.modules.common.functions`
ReferenceError: ColorUtils is not defined, on every render of every
reasoning segment. My own defect, shipped through a lint that said clean.
The lint was blind because I staged an approximation of the composed tree by
hand instead of linting the composed tree itself. Against the real composed
tree qmllint reports it plainly: 1 ColorUtils diagnostic before, 0 after.
Lint the composed tree. A hand-built stand-in tests the stand-in.
Config: sidebar.ai.fontSize restored
MessageTextBlock (vendor) reads Config.options.sidebar.ai.fontSize and got
undefined, assigning undefined to font.pixelSize on every text render.
Pre-existing, not from 0008 — the vendor delegate used MessageTextBlock too.
Cause is structural: modules/common/Config.qml SHADOWS ii-base's Config
wholesale, so every key ii-base adds after the fork silently disappears. The
key even carries an ii-base comment explaining it was added deliberately as
a live theme binding. Nothing failed; a number was just quietly absent.
Restored as a live binding, never a literal and never null
(null-in-a-serialized-config, 2026-08-11).
Verified: shell alive 12s after the Config edit (the crash-loop window),
config.json 46 keys and zero nulls, both fault lines gone from the log.
Applied 0009 then 0008 (0008's re-synthesize control calls a function 0009
adds). Casey applied and reloaded; verified against a running shell rather
than from a clean load.
0009 Speech.qml
stop() could not stop. playProc ran `sh -c "mpv ... || ffplay ..."` — a
compound command, so sh does not exec-replace itself and SIGTERM killed the
wrapper while the player kept sounding as an orphan. Reproduced directly.
That is the back-to-back TTS slam: every stop left audio playing and the
next speak started a second player over it. Shell dropped; the pid held is
now the pid making noise. ffplay fallback deleted rather than repaired —
needing a fallback is what forced the wrapper that broke the kill.
resynthesize() added, bypassing the cache: the button existed for "that came
out wrong" and, being keyed on the same text, always replayed the identical
file. synthesizing/playing split out of one `speaking` boolean.
0008 AiChat.qml
One line: delegate AiMessage -> AgentMessage. The vendor block named 8 tools;
the registry holds 19. The 11 it could not see were exactly the interiority
surface — outfit, nickname, subagent, atmosphere, reach, consult, itinerary,
todo, schedule, halt, intrusive.
regenerate and edit dropped deliberately, per Casey: text regeneration is not
possible (Ai.regenerate() already returned advice) and there is no in-place
edit (the vendor wrote to a local array the server never sees). delete is
armed and states that it hides locally only. Do not restore them.
Queue entries removed here, per the directory's own discipline.
The queue section claimed 0001-0004 were all unapplied. Verified with
git apply --check in both directions: 0003 landed as 8c434df, 0004 as
23cebc5, and 0002 can no longer apply at all.
0002 patched inThinkBlock in services/Ai.qml. That variable no longer
exists - typed segments made the think-fence collision impossible by
construction rather than by escaping harder. Retired, not abandoned.
0001 and 0006 were on disk and undescribed. Both now carry their
reasoning here. 0007's block records the specific installed version it
waits on, since 'needs the server first' is not actionable a month
later.
A queue is not an archive; git keeps history.
0008 was blocked because the owned delegate carried none of the vendor's
seven message controls. df11bba carries five and drops two deliberately;
the header now records which and why rather than only lifting the warning.
0009 depends on nothing but is a prerequisite for 0008's re-synthesize
control doing anything real.
Vendor AiMessage exposed seven controls; the owned delegate had none, which
made patch 0008 a parity regression rather than a swap. Five are carried, two
are deliberately dropped:
regenerate — the conversation is forward-only and Ai.regenerate() is already
a no-op returning advice. A button whose only behaviour is to explain that
it does nothing is worse than no button.
edit — there is no in-place edit. The vendor's wrote to a local array the
server never sees, so the message read back was not the message held.
delete is kept but armed: removeMessage() splices two local arrays and leaves
the server transcript untouched, so it is a view filter wearing a delete icon.
The armed row says so in words and replaces the controls in place rather than
opening a modal, which on a layer-shell surface would fight for focus.
speak and copy are separated at the source. copy takes the whole content;
speak takes text segments only, so the synthesizer no longer reads reasoning
and tool payloads aloud. Legacy messages without segments fall back to full
content rather than to silence.
re-synthesize prefers Speech.resynthesize() when present and degrades to the
old stop+speak otherwise, because speak() opens with a cache check on the text
and re-synthesis is by definition the same text — the legacy path replays the
identical broken audio.
deploy.sh: name the hazard the composer already implements — a new file
plus a manifest line is inert until the next deploy, while editing a file
already in the manifest is live in the running shell immediately.
Registration is the dangerous act, not creation.
patches/README: 0008 swaps the message delegate to one that does not carry
the vendor control row (regenerate, speak, re-synthesize, copy, edit, raw,
delete). Applying it would remove all seven.
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.