PR #1 (LlmProvider + OAuth) fixes:
- Move provider selection from [bifrost].provider to [inference].provider
(Bifrost is a provider, not the parent category — they are peers)
- Add Inference category to settings TUI with provider picker
- Wire build_provider() into CLI/chat/model-refresh paths so OAuth
works outside server mode
- Update ServerConversation to use Arc<dyn LlmProvider> for consistency
PR #2 (web UI) assessment:
- Remove entire web/ directory — not aligned with substrate ethos
(client-side compaction model contradicts Constitution Article IV;
autoCommit toggle misunderstands git-backed memory physics;
vocabulary doesn't match project architecture)
- Keep the 3 new REST endpoints (config, compaction-logs, token metrics)
- Revert run_reflect path change (keep canonical ~/.souveraine/agents/)
- Delete souveraine_fixes.patch (dev artifact)
- Restore demo example (was commented out as workaround for missing file)
- Copy examples/demo.rs from primary branch (was never pushed to public)
Tests: 184 passed, 0 failed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce an `LlmProvider` trait (the engine<->LLM seam, sibling to the
`Backend` harness<->engine trait) so inference can route to providers
beyond the Bifrost gateway. Two impls behind it:
- `BifrostClient` - existing OpenAI-compatible gateway (default).
- `OpenAiOAuthProvider` - rides the Codex CLI's ChatGPT login
(`~/.codex/auth.json`) and drives `chatgpt.com/backend-api/codex/responses`
(Responses API) with no API key. Self-refreshes the token (single-flight,
write-back, CLI re-read fallback) and translates the engine's OpenAI-chat
request to/from the Responses API + SSE accumulation.
Selected via `[bifrost] provider` ("bifrost" | "openai-oauth"). The engine
keeps speaking the existing ChatCompletionRequest/CompletionResult/
InferenceStrain currency, so all six inference call-sites are unchanged -
only the field type flips to `Arc<dyn LlmProvider>`.
Model ids are translated at the provider boundary (oauth/catalog.rs::resolve):
Bifrost-namespaced ids (`openai/...`, `-precision`) map onto served ChatGPT
models; `-fast` -> priority service tier.
Verified live to the wire level: builds+links, server boots in oauth mode
(reads the Codex token), and chatgpt.com accepts the request (auth, endpoint,
headers, payload all valid). The SSE->CompletionResult accumulation is NOT yet
verified against a successful completion (blocked by a subscription usage limit
at test time) - needs one live turn to confirm end-to-end.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirrors the primary's pattern at `src/server/turn.rs:445-471` — bifrost
returns the full response in one shot, we chop it into 10-char chunks
and emit each as a `SubconsciousToken` with a 20ms sleep between, so
the subconscious appears to be typing in real time.
The TUI side needed a small refit so the chunks don't each become their
own line in the stream Vec (a 10-char-wide vertical waterfall):
- `ChatState.subconscious_current: String` — the live-building line.
Token chunks `push_str` here. The render path shows it as the
brightest bottom line, ahead of finalized history lines above.
- `SubconsciousToolCall`/`SubconsciousToolResult` flush the buffer to
`subconscious_stream` (as a finalized line), clear it, then push
their own line. New `SubconsciousPass(active=true)` clears both.
- Render packs the visible window from newest backward, including the
live line as the bottom entry; old history lines above fade upward
toward `agent_dim` and gain the `DIM` modifier in the upper third.
If the stream receiver is dropped mid-chunk, the inner loop bails out
but the round itself completes — bifrost is already done, only the UX
narration is interrupted.
TUI — subconscious live stream:
- Add `subconscious_stream: Vec<String>` to `ChatState`; events.rs already
pushes here via `SubconsciousToken`/`SubconsciousToolCall`/`SubconsciousToolResult`.
- Render below the phase bar during `TurnPhase::Subconscious`. Newest at
bottom in `palette.surfacing`; older lines fade upward toward
`palette.agent_dim` via `lerp_color`; the oldest visible line gets the
`DIM` modifier on top. Render path now builds `Vec<Line>` instead of
collapsing into `Line::from(spans)` (the latter forced everything onto
one horizontal row, which was the visible "1 2 3 across" bug). Lines
are width-clipped with an ellipsis fallback.
Seeds — journal/ledger routing:
- Aster mandate Phase 3 now records journal entries via the `memory`
tool at `journal/YYYY/MM/DD.md` (relative to memory root), never via
`write`/`edit`. Previous phrasing read as a filesystem path and the
model resolved it against cwd — when souveraine runs from the project
tree that landed `journal/` and `memory/ledger/` directly in the
source tree instead of the agent's memfs.
- "How I record" section likewise anchors ledger reads/writes to the
`memory` tool's `append`/`read`/`write` commands.
Cargo:
- 0.1.0 → 1.0.0.
- tower-http 0.5 → 0.6, notify 6 → 7, crossterm 0.27 → 0.28.
Bug docs (`docs/bugs.md`):
- B-012: TUI doesn't auto-refresh after tool calls complete (event
pipeline missing a redraw kick after `BackendEvent::ToolResult`).
- B-013: Tool calls stop working mid-conversation — model returns
`tool_calls=0` for the rest of the session, no error or log.
Suspected: recent `defs.rs` env-var refactor changing `retain`
behavior on env vars, possibly nuking tool defs during schema gen.
- B-014: Subagent `run_in_background: true` blocks primary — the flag
is parsed but never read; `Subagent::execute` unconditionally awaits;
`SubagentPool` is a stub.
Task queue:
- Split old `live-subconscious-stream.md` into two scoped task files:
`subconscious-live-reasoning-stream.md` (broader design — ticker,
cockpit event log, agency-driven surfacing) and
`subconscious-streaming-line-render.md` (this PR's narrow render
wiring).
- New tasks: `prompt-override-system.md` (move hardcoded seeds into a
project `prompts/` truth source the agent can override via
`souveraine_operations/`), `pasted-content-formatting.md` (visual
marker for pasted vs composed text).
- `CLAUDE.md` high-impact bullet: replace stale `rename-aster-to-subconscious`
with `mid-turn-subconscious-checkpoint.md` (matches the c1f851f redesign
that ripped out the watchdog in favour of `halt`/`intrusive` tools).
Public-branch prep:
- `docs/audit/external-security-audit-prompt.md` — scoped prompt for an
external creds/PII/license sweep of the `public` branch.
- `commit-history-public.txt` — full public-branch log dump used as
reference for that audit pass.
Replace real name, email, and gendered pronouns in doc comments with
neutral phrasing ("the user", "Souveraine Contributors", "TestUser").
Remove absolute home directory path from tool defs doc comment.
Add souveraine.toml to .gitignore so it won't be re-tracked.
- Itinerary tool (set/advance/describe/clear) with YAML persistence
and todo-linked enrichment, rendered as TUI header strip
- Mid-turn subconscious checkpoint: every N tool rounds, Aster assesses
progress; HALT verdict breaks the loop with full correction pass
- Nickname tool: agent learns human's name via set/get/clear, stored
in system/human.md frontmatter, TUI uses it in chat bubble labels
- Removed hardcoded human.md write from memory::init (wizard owns it)
- Prompt nudge for itinerary when live todos exist without a route
- Left/Right arrows move cursor by char boundary, Backspace deletes at
cursor, Char inserts at cursor (not end-of-string)
- Home/End jump to start/end, Ctrl+Left/Right skip by word
- Cursor renders at the correct visual position (wrapping-aware)
- Bracketed paste detection (crossterm 0.27 feature) — pastes >3 lines
tagged with a system message so the agent sees pasted material
- Paste inserts at cursor position with no size cap
- docs/bugs.md updated: B-006 fixed
- CLAUDE.md, docs/tasks/INDEX.md updated for refactor status
- heartbeat-n1-after-autonomous, n100-archivist-compression moved to
tasks/archive/ — the first landed this session, the second was
already built before it.
- Dated session handoffs (May 14c/15/16) moved to archive/handoffs/.
- Rescued a stray bug log to archive/bugs.md and removed the stray
nested souveraine/ directory.
- Filed stray working files (an old chat.rs copy, a one-off script,
terminal-paste garbage, a prev log) under docs/archive/strays/,
which is gitignored — kept on disk, out of the repo.
- INDEX.md task list refreshed to drop archived references.
Sensorium: a new sensorium::ambient_line() builds a one-line ambient
sense — current date/time and the connecting user — prepended to the
user message every turn, for both the primary (send_with_signals) and
the subconscious (subconscious_tool_loop). The subconscious no longer
guesses the date; ledger entries get stamped with the real time.
TUI scroll-wheel: the chat viewport now responds to mouse ScrollUp/
ScrollDown, routed through the same scroll field the arrow keys move
(3 lines per notch).
TUI click-to-copy fix: copy_message_at was hit-testing against stale
line indices — span_spans recorded message positions against the
pre-wrap buffer, but wrap_lines reflows it and shifts every index past
a wrap point. draw_messages now builds a prefix-sum remap from pre- to
post-wrap indices and translates the spans before storing them.
arboard itself was fine; the bug was the bookkeeping.
Subconscious N+1: parse_observations now reads the natural
`- **label**: content` markdown the model actually produces. The rigid
source/content/urgency triple matched nothing the model emitted, so
every observation was dropped and the pass surfaced "no anomalies
detected" every time. Legacy triple kept as a fallback. Tests cover
the live output captured from a real pass.
Heartbeat N+1: autonomous-cycle surfacings stash to
pending-surfacings.jsonl (src/core/nervous/pending.rs) and replay in
the TUI/CLI on next connect, instead of draining silently into a
stream nobody reads.
First-run: souveraine init demoted to a config-template writer; the
TUI setup wizard is the onboarding path. The wizard now persists the
Bifrost URL/key/model it collects (to disk and the OS keyring) before
agent creation, so the next launch starts configured. Default agent
name changed from Ani to Souveraine.
Docs: alpha-tester-readiness, strip-emojis, and truncation-signal-polish
task notes.
Also sweeps in pre-existing uncommitted work on this branch
(bifrost.rs, compact/plan.rs, and parts of local.rs / main.rs).
Three turn-lifecycle bugs Casey hit in daily use.
④ Interjections: the backend drains the queue between tool rounds and
after the final LLM call, but one arriving during the chunked
stream-out — or after the turn ended — just sat in the queue, never
delivered ("she isn't getting that message"). On turn completion
(Done / channel close) any still-queued interjections now open a
fresh turn. submit() is split into spawn_turn() so this delivery
reuses the Interjection bubbles instead of duplicating a User bubble.
⑤ Agent switch mid-stream: select_agent() dropped the chat surface
without cancelling the running turn, so run_turn kept executing
orphaned and resurfaced as "doing the same message again" on a later
/resume. select_agent() now fires the turn's cancel token first
(cancel_active_turn) — the backend commits partial output and stops.
⑥ Resume-or-new: entering chat silently auto-created a conversation.
ChatState::connect now offers it — offer_resume_or_new() lists the
agent's prior conversations (excluding the just-created empty one)
and raises the ConversationPicker with an [n] new option; silent
when the agent has no prior conversation.
Casey reported model changes in Settings not taking effect on the agent
he meant. Root cause: Settings only ever edited the global
[bifrost] primary_model, and a save silently pushed that global change
onto whatever agent the open chat happened to be — never the agent the
user thought he was configuring, and nothing at all with no chat open.
- New AgModel field in the Agent settings category, backed by
ActiveAgentSettings (id, name, model) loaded from the active agent's
agent.json on Settings entry — so the field shows and edits THIS
agent's llm model, and follows agent switches.
- On save, an AgModel change is pushed to that agent via the backend's
update_agent_model (also refreshes cache + SQLite mirror).
- bifrost.primary_model is now purely the new-agent default; the
confusing primary_model -> active-agent cross-push is removed.
Audit found the default profile's subconscious running on a hollow stub
and the n1_enabled toggle wired to nothing. Fixes:
- New src/core/seeds.rs: SUBSTRATE_PROMPT (how the substrate works),
DEFAULT_PERSONA (grown-from template), DEFAULT_COVENANT, DEFAULT_STATE,
SUBCONSCIOUS_MANDATE, subconscious_persona().
- prompt.rs: build_system_prompt_full injects SUBSTRATE_PROMPT whenever
[agent] system_prompt is unset — every agent wakes knowing its world.
- agent_inventory.rs: new agents seeded with the grown-from persona +
covenant + state; subconscious seeded with real persona + four-fold
mandate + the six ledger files, no more placeholder stub.
- local.rs: n1_enabled now actually gates the N+1 pass (global AND
per-agent must be on). It was read nowhere before.
- Drop the unused kimi-k2.5-turbo model everywhere; default is now
openai/kimi-k2.6 (config.rs, main.rs template, both toml files, setup).
- chat.rs: complete the truncated wrap_text for the btw word-wrap fix.
Decouple the subconscious pass from the user-facing turn so she is
never held hostage to N+1:
- PrimaryComplete event releases the input the moment her response
commits; the N+1 pass runs behind it on the still-open stream
- on_response takes an owned session snapshot — no live DashMap ref
held across the (long) pass, so a concurrent next turn can't
deadlock on the shard lock
- TurnPhase::Subconscious — the status line reads "Subconscious",
not "Streaming" (bug B-002)
- the subconscious now persists her own Session across N+1 passes,
with her own compaction engine wired in
Rework the chat surface toward bubbles back and forth:
- tool calls fold into a footer on her bubble (Ctrl+T expands the
full cards); leading/trailing runs attach to the right bubble
- streaming tail fades toward the background with a live caret
- in-flight tools breathe cyan↔purple; settle to ✓/✗ outcome glyphs
- click any bubble to copy it — ⧉ title cue, arboard clipboard
- breathing room around tool runs; entry shimmer on new messages
souveraine is a binary — committing Cargo.lock gives reproducible
builds when installing on another machine and a stable dependency set
for the audit. Voice-input test recordings (voice-recording-*.mp3) are
transient and now ignored.
Streamed tokens no longer append straight to the visible message.
They land in a stream_buffer; advance_tick releases a proportional
slice each frame (~1/3 of what is waiting, min 8 bytes), so a burst
of tokens reveals as steady flow rather than a sudden block. The cut
prefers a nearby whitespace break so no partial word flashes.
finalize_streaming flushes the buffer so a turn never ends with
unrevealed text. The 30 FPS redraw decoupling was already in place.
Mid-turn text the model emits alongside tool calls is no longer a flat
italic line. A new Register splits it in two: a *cenno* — a short
ambient aside attached to tool work, kept quiet and italic — and
*her-voice* — a substantive passage rendered with a left gutter bar and
no italic, so it reads as her actual voice rather than a whisper.
The backend classifies by word count at emit time against the new
tui.cenno_word_threshold (default 30), exposed as an editable field in
the Settings TUI category.
A second sidebar component beneath the cockpit. Shows context pressure
(tiered bar), backend connectivity, N+1/N+25/N+100 cadence, last
compaction warning, inference strain (504/429/error tallies — the 504s
Casey kept seeing), energy and mood, plus session uptime in the title.
Pure accumulation from existing TuiEvents — no new backend wiring.
Aster is Ani's personal subconscious-agent name, not a framework
concept. The codebase now refers to the N+1 pass and its components as
subconscious everywhere — const names, fn names, variables, comments,
the schedule source tag, the todo source enum, and UI strings.
Mechanical rename, no behaviour change. grep -i aster src/ now returns
only Faster-Whisper. reference/ and docs/ historical context untouched.
ArchivistEngine scans journal entries written since the last synthesis,
sends them to a compression model, and writes a dense fragment to
system/synthesized/{end-date}.md with a covers-marker for idempotent
resume. ConsciousnessEngine::on_response calls maybe_synthesize — fires
on interval (maintenance) or pressure threshold (emergency), no-ops when
no journal entries are new. Replaces the placeholder pressure check.
This was uncommitted work-in-tree; preserved here as its own commit so
it is not lost or conflated with unrelated changes.
B5: every_n_responses and time_based triggers now expose their inner
value (count / interval secs) as a conditional sub-field — previously
stuck at the 5/3600 defaults with no way to edit.
Propagation: FieldLoc::applies_live() marks the three fields that reach
the running system immediately (atmosphere, outfit, primary_model); a
legend line states the rest take effect on restart.
- settings: open on Categories panel; "saved" message; fix ScMaxTokens
commit path; char-boundary-safe field editor (UTF-8 panic on paste)
- tui: drop empty interstitial gap lines (trim whitespace narration)
- compaction: teach the subconscious to compact (SUBCONSCIOUS_BODY_ORIENTATION);
memory compact resolves per-agent-type default; subconscious leans sliding_reflect
- sliding_reflect preservation pass runs as a fresh fork of the agent being
compacted — her persona, first person, '[Threads I carried forward]'
Adds Atmosphere::from_name() constructor and wires a readback of
system/preferences/visual.md at TUI conversation startup (alongside the
existing energy-balance read), so the agent's last explicit atmosphere
choice survives restarts.
Archives 5 resolved task docs:
- tui-clean-conversation-switch (switch_pending state machine in place)
- scope-4-n25-reflection (408-line engine, fires at N+25)
- tui-presence-and-interrupt (interjection queue, /btw, raise-hand,
phase display, self-awareness pulse all built)
- presence-visual-evolution (ChatPalette wired, posture-shift fix,
cross-session atmosphere restore now landed)
- presence-autonomy (atmosphere tool bidirectional, prefs readback now
wired; from_posture() coupling remains per proto task scope)
Updates CLAUDE.md active task queue to reflect current state.
FederationRole (hearth/limb) in config; bridge carries role in
device_announce; DeviceRegistry tracks role per peer with a split-brain
guard (two hearths → ERROR!). Role defaults to hearth: a standalone
machine is its own home.
reach/consult was classified by trusting the event_type field, so any
peer could claim "reach" and skip the consent gate. Federation also used
one per-machine seed for everything.
Split the two identities the codebase already had: the agent seed
(agents/{id}/seed/, travels with the memfs, identical across one agent's
machines) now signs the summon payload; the device seed keeps signing
the transport envelope. The receiver classifies reach vs consult by
verifying the agent signature against its own agent pubkey — a match is
genuinely self (reach, no consent gate), a mismatch is a separate being
(consult, consent-gated), an invalid signature is dropped. The lite
listener verifies the same way, reading hosted agent pubkeys straight
from seed/public.key with no engine load. authorized-summoners.md is now
keyed on agent pubkeys — consent is per-being, not per-machine.
New core/identity/summon.rs carries the signing helpers and round-trip,
tampered-field, and wrong-pubkey tests.
An inbound reach/consult now wakes the target agent with a background
turn instead of waiting for her next natural turn. SummonHandler holds
a OnceLock<Arc<dyn TurnInjector>>, wired from LocalBackend::new with the
same injector the heartbeat handler uses. Gated on FederationConfig
.auto_wake (sovereign default off). Only the inbound summon wakes; a
summon_response still surfaces in the caller's inbox per fire-and-surface
so the caller is never interrupted. The pure server path has no turn
loop — auto-wake there is a graceful no-op.
Phase 4 — Reach & Consult protocol:
- agent.rs rewritten as the `reach` (self-extension) and `consult`
(sovereign peer) tools; event-bus dispatch, peer resolution via
known_peers.json
- summon_handler: fixed inbox path, real pending/intrusive routing,
timeout surfacing to bus + inbox, outbound-request self-registration
- bridge always forwards control events regardless of subscriptions
- response loop closes via a file outbox: an inbound summon instructs
the agent to write federation/outbox/{id}.md; scan_outbox turns that
reply into a summon_response routed back to the caller
Phase 5 — Lite listener:
- `souveraine listen` — minimal federation presence with a summon-wake
watcher; parks authorized summons to .summon-pending/
- full server drains .summon-pending/ on startup
- FederationConfig: authorized_summoners, auto_wake
Phase 6 — Memory gating (partial):
- federation posture injected into the system prompt from the
federation/ memfs contract
- consent floor via authorized-summoners.md
- device registry: prune_stale timer, first_seen preserved
Builds clean (0 errors). Status + deferred work documented in
docs/tasks/federation-summon.md. Also sweeps in pre-existing in-flight
working-tree changes (TUI, prompt, docs).
DeviceRegistry tracks known federated peers from device_announce/device_leave
SensorEvents on the bus, persisted to ~/.souveraine/federation/known_peers.json
for CLI access. Bridge emits device_announce on connect. New `souveraine peers`
subcommand lists known peers. Wired into SouveraineServer at construction.
SensorEvent gets reply_to for directed routing. FederationConfig gains
peers (url + pubkey + subscriptions). New src/server/federation/ module
with SignedEvent (Ed25519 sign/verify) and FederationBridge (per-peer
outbound WS tasks with echo-safe seed_id filter and backoff reconnect).
Inbound /v1/federation/events handler verifies signatures before bus
injection. Bridge spawned in server::run() when federation.enabled.