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.
Remove the auto-reset stall guard that silently killed turns after 90s
and orphaned the backend. The TUI now trusts the stream: backend emits
Keepalive every ~15s between rounds, the phase strip shows a liveness
label (waiting Ns... / still waiting Ns...), and the user decides when
to Esc. SSE ping frames map to Keepalive on remote backend.
Instance registry no longer blanket-registers all agents at startup —
register_instance takes an explicit agent_id, called from
new_conversation only. Fixes inflated instance counts on manager cards.
Consciousness engine (Aster) looks up the primary agent's name
dynamically instead of hardcoding "Ani". Parameter renamed to
primary_response.
Interjection labels changed from /btw to hand-raise metaphor
(hand raised → noticed) since /btw is its own fork feature.
- Adds owner_seed_id column to agents table, stamped from instance Ed25519
seed on agent creation (Path B ownership model)
- Splits routes into public (list/create agents, health) and protected
(update/delete agent, get conversation, stream messages) with per-agent
bearer token middleware
- Factors verify_token() helper from existing memory-route middleware;
adds require_agent_token() and require_conversation_token() wrappers
- RemoteBackend loads per-agent token from disk and sends Authorization
header on protected requests; ChatState recreates backend with token
after discovering agent_id
- Atmosphere system (src/ui/atmosphere.rs): 14 color presets ported from
the Matrix adapter system, wired into Presence with posture-linked
defaults and explicit BackendEvent::Atmosphere trigger path
- Welcome portrait: decoupled from 18×18 pixel-art constants, now sized
at ~40% of terminal width (capped 48), proper Resize::Fit rendering
- Dashboard overlay: moved from TopRight to BottomRight so it no longer
overlaps the 4th dashboard card
- Streaming staleness guard: 30s timeout in drain_events — if no
BackendEvent arrives while busy, the turn resets to Idle and injects
a system message instead of displaying "Streaming" indefinitely
- Hal agent: registered as d91e264c-bd5a-4d02-9641-9202b9a64be5 with
full memfs, seed, portrait, and DB entry
- Expression cache (src/ui/expressions.rs): pre-existing but uncommitted
241-line module for per-agent expression frames with fallback chain
- Fix borrow errors in session_manager.rs and chat.rs BtwState match
- BackendEvent::Atmosphere variant + TuiEvent::AtmosphereChanged wiring
- Agent manager: full card grid with stateful photo portraits (Resize::Fit),
Letta-style metadata blocks (glyph, name, path, stats), PRIMARY/ACTIVE/idle
badges, auto column count 1-4
- Welcome screen: pulls active agent portrait from same card-image cache,
replaces the broken eager-load Protocol path
- Chat phase strip: dedicated 1-row line between messages and input showing
⏣ Thinking · 12s / Running tool · 4 tools used / Streaming / × Interrupted
- Always-on input: input box is live during busy — Enter queues an interjection
instead of being rejected
- /btw <text>: slash command for explicit mid-turn messages
- Backend interjection queue: Arc<Mutex<Vec<String>>> drained between LLM rounds
and prepended as [user interjected at HH:MM] system notes
- Esc → interrupt still works; phase strip shows × Interrupted