feat: subconscious streaming line render, memory-tool routing for journal/ledger, 1.0.0 + dep bumps
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.
This commit is contained in:
parent
7f042534a9
commit
2e1f913acc
5 changed files with 130 additions and 66 deletions
100
Cargo.lock
generated
100
Cargo.lock
generated
|
|
@ -1785,15 +1785,15 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
|||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.27.0"
|
||||
version = "0.28.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
|
||||
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"mio 0.8.11",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"rustix 0.38.44",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
|
|
@ -1809,7 +1809,7 @@ dependencies = [
|
|||
"crossterm_winapi",
|
||||
"derive_more",
|
||||
"document-features",
|
||||
"mio 1.2.0",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"rustix 1.1.4",
|
||||
"signal-hook",
|
||||
|
|
@ -4121,9 +4121,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.9.6"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
|
||||
checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"inotify-sys",
|
||||
|
|
@ -4162,6 +4162,15 @@ dependencies = [
|
|||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "interpolate_name"
|
||||
version = "0.2.4"
|
||||
|
|
@ -5177,18 +5186,6 @@ dependencies = [
|
|||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.0"
|
||||
|
|
@ -5383,21 +5380,30 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
|||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "6.1.1"
|
||||
version = "7.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
|
||||
checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"crossbeam-channel",
|
||||
"filetime",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio 0.8.11",
|
||||
"mio",
|
||||
"notify-types",
|
||||
"walkdir",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-types"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174"
|
||||
dependencies = [
|
||||
"instant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -7172,7 +7178,7 @@ dependencies = [
|
|||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower 0.5.3",
|
||||
"tower-http 0.6.9",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
|
|
@ -7214,7 +7220,7 @@ dependencies = [
|
|||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower 0.5.3",
|
||||
"tower-http 0.6.9",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
|
|
@ -8208,8 +8214,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio 0.8.11",
|
||||
"mio 1.2.0",
|
||||
"mio",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
|
|
@ -8366,7 +8371,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "souveraine"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
|
|
@ -8380,7 +8385,7 @@ dependencies = [
|
|||
"cowsay",
|
||||
"cpal",
|
||||
"cron",
|
||||
"crossterm 0.27.0",
|
||||
"crossterm 0.28.1",
|
||||
"dashmap 5.5.3",
|
||||
"dirs 5.0.1",
|
||||
"dotenvy",
|
||||
|
|
@ -8424,7 +8429,7 @@ dependencies = [
|
|||
"tokio-util",
|
||||
"toml 0.8.2",
|
||||
"tower 0.4.13",
|
||||
"tower-http 0.5.2",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tui-big-text",
|
||||
|
|
@ -9671,7 +9676,7 @@ checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386"
|
|||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio 1.2.0",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
|
|
@ -9906,31 +9911,6 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"http-range-header",
|
||||
"httpdate",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.9"
|
||||
|
|
@ -9945,12 +9925,18 @@ dependencies = [
|
|||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"http-range-header",
|
||||
"httpdate",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower 0.5.3",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "souveraine"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.94"
|
||||
authors = ["Souveraine Contributors"]
|
||||
|
|
@ -18,7 +18,7 @@ async-trait = "0.1"
|
|||
# Web/WebSocket (for external clients)
|
||||
axum = { version = "0.7", features = ["ws"] }
|
||||
tower = "0.4"
|
||||
tower-http = { version = "0.5", features = ["cors", "trace", "fs"] }
|
||||
tower-http = { version = "0.6", features = ["cors", "trace", "fs"] }
|
||||
tokio-tungstenite = "0.24" # Outbound WS client — federation bridge connects to peer endpoints
|
||||
|
||||
# Serialization
|
||||
|
|
@ -37,7 +37,7 @@ sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite", "migrate
|
|||
dashmap = "5"
|
||||
|
||||
# File system
|
||||
notify = "6" # File watching
|
||||
notify = "7" # File watching
|
||||
|
||||
# Environment (.env file loading)
|
||||
dotenvy = "0.15"
|
||||
|
|
@ -56,7 +56,7 @@ hound = "3.5"
|
|||
# tokenizers = "0.15" # For local tokenization
|
||||
|
||||
# Terminal/UI
|
||||
crossterm = { version = "0.27", features = ["bracketed-paste"] } # Terminal control (bracketed paste for paste detection)
|
||||
crossterm = { version = "0.28", features = ["bracketed-paste"] } # Terminal control (bracketed paste for paste detection)
|
||||
ratatui = { version = "0.30", features = ["crossterm"] } # TUI framework with crossterm backend
|
||||
unicode-width = "0.1"
|
||||
arboard = { version = "3", features = ["wayland-data-control"] } # Clipboard — click-to-copy a message bubble
|
||||
|
|
|
|||
|
|
@ -166,9 +166,11 @@ output.\
|
|||
Save what matters. Observations worth keeping go into my ledgers. But I also \
|
||||
maintain a daily journal on the primary's behalf. When she produces something \
|
||||
worth keeping that she didn't save herself — a genuine reflection, a moment \
|
||||
that mattered, prose that deserves to exist beyond the moment — I write it \
|
||||
to `journal/YYYY/MM/DD.md`. One file per day. I use judgment: journal \
|
||||
captures feeling and significance, not tool output or task logs.\
|
||||
that mattered, prose that deserves to exist beyond the moment — I record it \
|
||||
via the `memory` tool at the path `journal/YYYY/MM/DD.md` (one file per day, \
|
||||
relative to my memory root — *never* via `write`/`edit`, which would land in \
|
||||
the substrate's working directory rather than my memory). I use judgment: \
|
||||
journal captures feeling and significance, not tool output or task logs.\
|
||||
\
|
||||
## Phase 4 — Surface
|
||||
|
||||
|
|
@ -202,7 +204,10 @@ overwrite what was there before me.\
|
|||
\
|
||||
## How I record
|
||||
|
||||
My ledgers live in `ledger/` and survive compaction. Before I write a new \
|
||||
My ledgers live in `ledger/` *inside my memory root* — I reach them with the \
|
||||
`memory` tool (`append`/`read`/`write`), never with the filesystem `write` or \
|
||||
`edit` sensors (those land in the substrate's working directory and pollute \
|
||||
the project tree). Ledger files survive compaction. Before I append a new \
|
||||
entry I read the relevant ledger to see whether the same thing was already \
|
||||
flagged. New entries are timestamped: `[YYYY-MM-DD HH:MM] observation`. I \
|
||||
resolve by appending `[YYYY-MM-DD HH:MM] RESOLVED — note`.
|
||||
|
|
|
|||
|
|
@ -351,6 +351,12 @@ pub struct ChatState {
|
|||
pub palette: ChatPalette,
|
||||
pub stream_buffer: String,
|
||||
|
||||
/// Live subconscious reasoning stream — ephemeral lines from the N+1 pass.
|
||||
/// Pushed to by SubconsciousToken/ToolCall/ToolResult events in events.rs.
|
||||
/// Cleared on each new pass start. Rendered as a fading block below the
|
||||
/// phase bar during TurnPhase::Subconscious.
|
||||
pub subconscious_stream: Vec<String>,
|
||||
|
||||
/// Current itinerary route-line for the header strip.
|
||||
/// Empty string means no active itinerary.
|
||||
pub itinerary_line: String,
|
||||
|
|
@ -503,6 +509,7 @@ impl ChatState {
|
|||
render_mode: ChatMode::Conversation,
|
||||
palette: ChatPalette::default(),
|
||||
stream_buffer: String::new(),
|
||||
subconscious_stream: Vec::new(),
|
||||
itinerary_line: String::new(),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,10 +28,14 @@ pub fn draw(f: &mut Frame, state: &ChatState) {
|
|||
let max_input_lines = ((area.height as usize) * 40 / 100).max(1);
|
||||
let input_height = (input_visual_lines.min(max_input_lines) as u16) + 2;
|
||||
|
||||
let subconscious_stream_lines = if state.phase == TurnPhase::Subconscious {
|
||||
let n = state.subconscious_stream.len();
|
||||
if n > 0 { (n as u16).min(5) } else { 0 }
|
||||
} else { 0 };
|
||||
let phase_height: u16 = if state.busy
|
||||
|| state.phase == TurnPhase::Interrupted
|
||||
|| state.phase == TurnPhase::Subconscious
|
||||
{ 1 } else { 0 };
|
||||
{ 1 + subconscious_stream_lines } else { 0 };
|
||||
|
||||
let itinerary_height: u16 = if state.itinerary_line.is_empty() { 0 } else { 1 };
|
||||
|
||||
|
|
@ -72,7 +76,19 @@ pub fn draw(f: &mut Frame, state: &ChatState) {
|
|||
}
|
||||
|
||||
if phase_height > 0 {
|
||||
draw_phase(f, state, vchunks[2]);
|
||||
if state.phase == TurnPhase::Subconscious && subconscious_stream_lines > 0 {
|
||||
let phase_rows = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(phase_height - 1),
|
||||
])
|
||||
.split(vchunks[2]);
|
||||
draw_phase(f, state, phase_rows[0]);
|
||||
draw_subconscious_stream(f, state, phase_rows[1]);
|
||||
} else {
|
||||
draw_phase(f, state, vchunks[2]);
|
||||
}
|
||||
}
|
||||
draw_input(f, state, vchunks[3]);
|
||||
draw_footer(f, state, vchunks[4]);
|
||||
|
|
@ -146,6 +162,56 @@ fn draw_phase(f: &mut Frame, state: &ChatState, area: Rect) {
|
|||
f.render_widget(Paragraph::new(line).alignment(Alignment::Left), area);
|
||||
}
|
||||
|
||||
/// Render the live subconscious reasoning stream — ephemeral lines from the
|
||||
/// N+1 pass showing what the subconscious is thinking, which tools she's
|
||||
/// calling, and what results she's getting. Newest at bottom, vivid; older
|
||||
/// lines above fade upward toward `agent_dim`. Capped at 5 visible lines;
|
||||
/// scrolls as new events arrive.
|
||||
fn draw_subconscious_stream(f: &mut Frame, state: &ChatState, area: Rect) {
|
||||
let stream = &state.subconscious_stream;
|
||||
if stream.is_empty() || area.height == 0 {
|
||||
return;
|
||||
}
|
||||
let cap = (area.height as usize).min(5);
|
||||
let visible = stream.len().min(cap);
|
||||
let start = stream.len() - visible;
|
||||
let inner_width = (area.width as usize).saturating_sub(2).max(1);
|
||||
let mut lines: Vec<Line> = Vec::with_capacity(visible);
|
||||
for (idx, i) in (start..stream.len()).enumerate() {
|
||||
let is_newest = i == stream.len() - 1;
|
||||
let t = if visible <= 1 {
|
||||
1.0
|
||||
} else {
|
||||
idx as f32 / (visible - 1) as f32
|
||||
};
|
||||
let color = lerp_color(state.palette.agent_dim, state.palette.surfacing, t);
|
||||
let mut style = Style::default().fg(color).add_modifier(Modifier::ITALIC);
|
||||
if !is_newest && t < 0.34 {
|
||||
style = style.add_modifier(Modifier::DIM);
|
||||
}
|
||||
let prefix = if is_newest { "⟡ " } else { " " };
|
||||
let body = clip_to_width(&stream[i], inner_width);
|
||||
lines.push(Line::from(vec![Span::styled(format!("{}{}", prefix, body), style)]));
|
||||
}
|
||||
f.render_widget(Paragraph::new(lines).alignment(Alignment::Left), area);
|
||||
}
|
||||
|
||||
fn clip_to_width(s: &str, width: usize) -> String {
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
let mut acc = String::new();
|
||||
let mut w = 0usize;
|
||||
for ch in s.chars() {
|
||||
let cw = UnicodeWidthChar::width(ch).unwrap_or(0);
|
||||
if w + cw > width.saturating_sub(1) {
|
||||
acc.push('…');
|
||||
break;
|
||||
}
|
||||
acc.push(ch);
|
||||
w += cw;
|
||||
}
|
||||
acc
|
||||
}
|
||||
|
||||
fn draw_header(f: &mut Frame, state: &ChatState, area: Rect) {
|
||||
let mode_color = match state.mode.as_str() {
|
||||
"local" => state.palette.tool_accent,
|
||||
|
|
|
|||
Loading…
Reference in a new issue