huskies: merge 1143 story Decouple LLM environmental awareness from chat transport — persona-keyed sessions and a real-time event subscription

This commit is contained in:
dave
2026-05-18 16:52:45 +00:00
parent e58ff4465a
commit fb4e52dd09
15 changed files with 281 additions and 77 deletions
+29 -31
View File
@@ -1,10 +1,10 @@
//! Read/write helpers for the `llm_sessions` LWW-map collection, including the
//! atomic `assemble_and_advance_session` helper used by the Matrix bot.
//! atomic `assemble_and_advance_session` helper used by every chat transport.
//!
//! LLM sessions are keyed by `session_id` (typically a Matrix room ID) and track
//! per-sled high-water marks so that `assemble_and_advance_session` can inject
//! only events the LLM has not yet seen and advance the marks atomically within
//! a single CRDT lock acquisition.
//! LLM sessions are keyed by **persona name** (e.g. `"timmy"` for the
//! gateway-level bot) and track per-sled high-water marks so that
//! `assemble_and_advance_session` can inject only events the LLM has not yet
//! seen and advance the marks atomically within a single CRDT lock acquisition.
use std::collections::{BTreeMap, BTreeSet};
@@ -16,16 +16,15 @@ use super::super::state::{apply_and_persist, get_crdt, rebuild_llm_session_index
use super::super::types::{LlmSessionCrdt, LlmSessionView, ScopeFilter};
use super::event_log::GAP_PIPELINE_EVENT;
/// Write or upsert an LLM session entry keyed by `session_id`.
/// Write or upsert an LLM session entry keyed by `persona`.
///
/// Creates a new entry if `session_id` is not yet present; updates
/// `persona_name` and `scope` on an existing entry. The `high_water`
/// register is not touched by this function — use `assemble_and_advance_session`
/// to advance it atomically.
/// Creates a new entry if `persona` is not yet present; updates `scope` on an
/// existing entry. The `high_water` register is not touched by this function —
/// use `assemble_and_advance_session` to advance it atomically.
///
/// The `scope` string must be in wire form: `"all"` for [`ScopeFilter::All`]
/// or `"sleds:hex1,hex2"` for [`ScopeFilter::Sleds`].
pub fn write_llm_session(session_id: &str, persona_name: &str, scope: &str) {
pub fn write_llm_session(persona: &str, scope: &str) {
let Some(state_mutex) = get_crdt() else {
return;
};
@@ -33,19 +32,19 @@ pub fn write_llm_session(session_id: &str, persona_name: &str, scope: &str) {
return;
};
if let Some(&idx) = state.llm_session_index.get(session_id) {
if let Some(&idx) = state.llm_session_index.get(persona) {
apply_and_persist(&mut state, |s| {
s.crdt.doc.llm_sessions[idx]
.persona_name
.set(persona_name.to_string())
.set(persona.to_string())
});
apply_and_persist(&mut state, |s| {
s.crdt.doc.llm_sessions[idx].scope.set(scope.to_string())
});
} else {
let entry: JsonValue = json!({
"session_id": session_id,
"persona_name": persona_name,
"session_id": persona,
"persona_name": persona,
"scope": scope,
"high_water": "{}",
})
@@ -57,19 +56,19 @@ pub fn write_llm_session(session_id: &str, persona_name: &str, scope: &str) {
}
}
/// Read a single LLM session entry by `session_id`.
pub fn read_llm_session(session_id: &str) -> Option<LlmSessionView> {
/// Read a single LLM session entry by persona name.
pub fn read_llm_session(persona: &str) -> Option<LlmSessionView> {
let state_mutex = get_crdt()?;
let state = state_mutex.lock().ok()?;
let &idx = state.llm_session_index.get(session_id)?;
let &idx = state.llm_session_index.get(persona)?;
extract_llm_session_view(&state.crdt.doc.llm_sessions[idx])
}
/// Atomically read new event-log entries for `session_id` past the stored
/// Atomically read new event-log entries for `persona` past the stored
/// high-water marks, render them as a block of audit lines, and advance the
/// marks to prevent double-injection on the next call.
///
/// The set of sleds whose events are collected is determined by the session's
/// The set of sleds whose events are collected is determined by the persona's
/// [`ScopeFilter`]:
/// - [`ScopeFilter::All`]: events from every sled present in the event log are
/// included — this is the gateway-level persona default that gives a full
@@ -81,7 +80,7 @@ pub fn read_llm_session(session_id: &str) -> Option<LlmSessionView> {
///
/// Returns an empty `Vec` when there are no new events or the CRDT is not
/// initialised.
pub fn assemble_and_advance_session(session_id: &str) -> Vec<String> {
pub fn assemble_and_advance_session(persona: &str) -> Vec<String> {
let local_sled_id = crate::crdt_state::our_node_id().unwrap_or_default();
let Some(state_mutex) = get_crdt() else {
@@ -91,9 +90,8 @@ pub fn assemble_and_advance_session(session_id: &str) -> Vec<String> {
return Vec::new();
};
// Determine the session's scope filter and current high-water map.
let (scope_filter, current_high_water) = match state.llm_session_index.get(session_id).copied()
{
// Determine the persona's scope filter and current high-water map.
let (scope_filter, current_high_water) = match state.llm_session_index.get(persona).copied() {
Some(idx) => {
let filter = parse_scope(&state.crdt.doc.llm_sessions[idx], &local_sled_id);
let hw = parse_high_water(&state.crdt.doc.llm_sessions[idx]);
@@ -168,8 +166,8 @@ pub fn assemble_and_advance_session(session_id: &str) -> Vec<String> {
}
let new_hw_json = serde_json::to_string(&new_high_water).unwrap_or_else(|_| "{}".to_string());
// Upsert the session entry with the new high-water value.
let idx_opt = state.llm_session_index.get(session_id).copied();
// Upsert the persona entry with the new high-water value.
let idx_opt = state.llm_session_index.get(persona).copied();
if let Some(idx) = idx_opt {
apply_and_persist(&mut state, |s| {
s.crdt.doc.llm_sessions[idx]
@@ -179,8 +177,8 @@ pub fn assemble_and_advance_session(session_id: &str) -> Vec<String> {
} else {
let scope_str = scope_filter.to_scope_str();
let entry: JsonValue = json!({
"session_id": session_id,
"persona_name": "",
"session_id": persona,
"persona_name": persona,
"scope": scope_str,
"high_water": new_hw_json,
})
@@ -191,8 +189,8 @@ pub fn assemble_and_advance_session(session_id: &str) -> Vec<String> {
state.llm_session_index = rebuild_llm_session_index(&state.crdt);
}
// Observability: log event-log size and gap count across the session's
// target sleds (the scope actually assembled for this session).
// Observability: log event-log size and gap count across the persona's
// target sleds (the scope actually assembled for this persona).
let total_entries = state
.crdt
.doc
@@ -211,7 +209,7 @@ pub fn assemble_and_advance_session(session_id: &str) -> Vec<String> {
})
.count();
crate::slog!(
"[event-log] assemble session={session_id} sled_entries={total_entries} gap_count={gap_count}"
"[event-log] assemble persona={persona} sled_entries={total_entries} gap_count={gap_count}"
);
// Render each new event as a compact audit line; gap sentinels get a