- 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
ratatui-image v11 provides kitty/sixel/halfblock rendering. Loads the
agent's portrait photo as a terminal image protocol alongside the existing
half-block PortraitSource. On supportng terminals (Kitty, WezTerm, iTerm2,
Ghostty) the Welcome screen shows the actual photograph instead of the
18x18 pixel-art downsample.
Also includes the Agent Manager screen (press `i` on Welcome) with
per-agent card data: seed glyph, uptime %, instance count, memory files,
pubkey prefix, description.
Photo portraits were stretched into a square (resize_exact to 18×18) and
lost all detail. Fix doubles the problem:
- Cover-crop: scale so the shorter axis fills the target, center-crop the
excess — landscape or portrait photos now fill the frame without distortion.
- 3× source resolution: store at 54×54 and bilinearly interpolate at render
time, so each of the 18×18 grid cells is a weighted blend of ~9 source
pixels instead of a single hard-sampled one.
Schema (idempotent migration in server/db.rs::init_database):
- agent_instances table (agent_id, instance_id, pid, hostname,
started_at, last_seen_at), primary key (agent_id, instance_id)
- lifetime_active_seconds INTEGER column on agents (added via
add_column_if_missing — SQLite has no IF NOT EXISTS on columns,
so PRAGMA table_info is checked first)
- idx_instances_agent index on (agent_id, last_seen_at DESC)
AgentInventory wires the lifecycle:
- register_instance(instance_id): prunes rows older than 5 min, then
INSERTs (or upserts last_seen_at) for every known agent
- heartbeat_instance(instance_id, tick_seconds): UPDATEs last_seen_at
and increments lifetime_active_seconds by tick_seconds for every
agent this instance has rows for
- instance_count(agent_id): COUNT of live rows (>=5min recent)
- lifetime_active_seconds(agent_id): cumulative active seconds
SouveraineServer::new generates a process-lifetime instance_id (UUID),
calls register_instance once, then spawns a 30s heartbeat loop. Both
local CLI mode and full server mode get the same registration path
(the manager-mockup "N Instances" badge will count rows; uptime % is
int(100 * lifetime_active_seconds / age_seconds), capped at 99 in UI).
Verified end-to-end via `souveraine agents`: both Annie and Souveraine
rows land with matching instance_id, current pid, hostname.
Build clean. 111/111 tests pass.
Each agent now has its own Ed25519 keypair at
~/.souveraine/agents/{uuid}/seed/, alongside its memfs. AgentInventory
gains seed_dir() / seed_id() getters and auto-initializes the per-agent
seed in create(). Federation can now reference agent identities directly
instead of routing everything through the host seed.
SeedId::glyph() produces a deterministic 4-char rendering of the pubkey
drawn from a 16-glyph geometric-shapes palette — terminal-friendly badge
for the manager-card SeedID display (and future federation peer lists).
CLI redesign — `souveraine identity` is now per-agent by default:
souveraine identity show # current agent's seed
souveraine --agent Annie identity show # Annie's seed
souveraine identity show --host # machine seed (federation)
souveraine identity sign "msg" --agent Annie
souveraine identity verify --pubkey ... --signature ... --message ...
The global --agent flag is now Option<String> with no default (was
hardcoded "Ani"). When omitted, the CLI falls through to the first
available agent on disk via first_agent_id_on_disk(), which walks the
user-side memfs dir and filters orphans by requiring a memory/
subdirectory. run_chat / run_tui / run_reflect / run_schedule all
updated to take Option<String> and pass through to existing
first-available fallback logic.
Help text example replaced "souveraine --agent Ani chat" with the
neutral "souveraine --agent <name> chat".
Build clean. 111/111 tests pass (gained 2 for glyph rendering).
Solves the split-data architecture problem. Previously the same agent
had two independent git repos: ~/.souveraine/agents/{id}/memory/ (used
by MemoryRepo::new_default — CLI + prompt builder) and
~/.souveraine/server/agents/{id}/memory.git/ (used by AgentInventory —
consciousness engine + API handlers). They diverged: writes through one
path never reached the other, so Aster's writes and Casey's CLI writes
landed in different repos for the same agent.
AgentInventory now distinguishes:
- agents_dir (server-managed): agent.json + conversations/
- memfs_dir (user-side, canonical): memory/ — the single source of truth
at ~/.souveraine/agents/{id}/memory/
memory_repo() / memory_root() now point at memfs_dir/{id}/memory/.
create() initializes the memfs there; update(), commit(), and
load_memory_blocks() write there. delete() removes both server-side
and memfs trees. Subconscious agents keep the memory.git/ convention
under subconscious-agents/ since they're not duplicated.
Removed the dual-path probe in PresenceState::load_portrait_from_memfs
that probed both memory/ and memory.git/ siblings — only memory/ now.
Build clean. 109/109 tests pass.
Broad-stroke pen-adjustment landing four pieces in one commit.
1. Bifrost tool-call schema (unblocks turns + Aster + reflection)
BifrostMessage now carries optional tool_calls / tool_call_id / name
with skip_serializing_if=None so unrelated turns keep the {role,content}
wire shape. All three tool loops rewritten to push real OpenAI tool-use
shape instead of a stringified JSON blob in content: backend/local.rs
(Ani's loop + LocalSubagentRunner), server/consciousness_engine.rs
(Aster's N+1 pass), core/reflection/mod.rs (N+25). This is the root of
the "first memory tool works, then everything fails on turn 2" symptom
and the silent Aster pipeline failure — same wire bug, three loops.
2. Welcome avatar dedup + scale-up
Presence overlay was rendering on both Welcome and Dashboard, doubling
up with the centered inline portrait on Welcome. Gated to Dashboard
only. Welcome portrait now uses render_scaled at WELCOME_SCALE=2 —
visibly larger, the focal point of the landing screen.
3. PNG portrait diagnostics
Silent .ok()? failures replaced with tracing::warn! showing the actual
decode error. load_portrait_from_memfs probes the sibling
memory.git/assets/ layout in addition to memory/assets/ since
server-managed agents live under memory.git/.
4. Tool-call rendering polish
New BackendEvent::ToolCall { id, name, arguments, round } and
BackendEvent::ToolResult { id, name, output, is_error } replace the
literal "🔧 Round 1 — executing: Bash, Memory" Token text spew.
ChatMessage::Tool variant + ToolResultBlock carry card state.
render_tool_card draws bordered cards: status glyph (⟳/✓/⚠), header
with name and round, compact JSON arg summary, output preview through
markdown.rs (12 lines + "… (N more)" tail). Errors paint red.
Build clean. 109/109 tests pass.
Docs: archive prior handoffs to docs/archive/handoffs/, refresh
HANDOFF_NEXT.md, add four new task files (tui-message-interactions,
agent-profile-and-repo-manager, per-agent-seed-and-instances,
code-mode-tool-rendering) tracking the next-instance work.