update: SessionId / SessionStart / SessionOrigin 等を Segment 系名称へ
- Type/Function/Variantを Segment* 系へ統一 - SessionId/SessionStart/SessionOrigin/SessionStartState/SessionState/SessionLogSink/SessionLockInfo - new_session_id / session_id / create_session* / list_sessions / lookup_session / update_session / find_by_session - protocol Event::SessionRotated → SegmentRotated、CompactDone.new_session_id → new_segment_id - Module: session_log → segment_log / session → segment (file mv 含む) pod 側の session_log_sink → segment_log_sink も同様 - crate 名 (session-store)、CLI flag (--session)、ResumeWithSession (CLI tied) は据え置き - session-tests/session_metrics_test 等の Store impl も追従
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! [`TraceEntry`] captures every LLM stream event verbatim for debugging
|
||||
//! and post-hoc analysis. Written to a separate `.trace.jsonl` file,
|
||||
//! completely independent of the session log used for state restoration.
|
||||
//! completely independent of the segment log used for state restoration.
|
||||
//!
|
||||
//! Disabled by default. Enable via `SessionConfig::record_event_trace`.
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
//! Filesystem-backed JSONL store.
|
||||
//!
|
||||
//! Layout:
|
||||
//! - Session log: `{root}/{session_id}.jsonl`
|
||||
//! - Event trace: `{root}/{session_id}.trace.jsonl`
|
||||
//! - Segment log: `{root}/{segment_id}.jsonl`
|
||||
//! - Event trace: `{root}/{segment_id}.trace.jsonl`
|
||||
|
||||
use crate::SessionId;
|
||||
use crate::SegmentId;
|
||||
use crate::event_trace::TraceEntry;
|
||||
use crate::session_log::LogEntry;
|
||||
use crate::segment_log::LogEntry;
|
||||
use crate::store::{Store, StoreError};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
@@ -14,7 +14,7 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
/// Filesystem-backed JSONL store.
|
||||
///
|
||||
/// Each session is stored as a single `.jsonl` file with one [`LogEntry`]
|
||||
/// Each segment is stored as a single `.jsonl` file with one [`LogEntry`]
|
||||
/// per line. Writes use append mode for crash safety.
|
||||
#[derive(Clone)]
|
||||
pub struct FsStore {
|
||||
@@ -30,11 +30,11 @@ impl FsStore {
|
||||
Ok(Self { root })
|
||||
}
|
||||
|
||||
fn log_path(&self, id: SessionId) -> PathBuf {
|
||||
fn log_path(&self, id: SegmentId) -> PathBuf {
|
||||
self.root.join(format!("{id}.jsonl"))
|
||||
}
|
||||
|
||||
fn trace_path(&self, id: SessionId) -> PathBuf {
|
||||
fn trace_path(&self, id: SegmentId) -> PathBuf {
|
||||
self.root.join(format!("{id}.trace.jsonl"))
|
||||
}
|
||||
|
||||
@@ -65,12 +65,12 @@ impl FsStore {
|
||||
}
|
||||
|
||||
impl Store for FsStore {
|
||||
fn append(&self, id: SessionId, entry: &LogEntry) -> Result<(), StoreError> {
|
||||
fn append(&self, id: SegmentId, entry: &LogEntry) -> Result<(), StoreError> {
|
||||
let line = serde_json::to_string(entry)?;
|
||||
self.append_line(&self.log_path(id), &line)
|
||||
}
|
||||
|
||||
fn read_all(&self, id: SessionId) -> Result<Vec<LogEntry>, StoreError> {
|
||||
fn read_all(&self, id: SegmentId) -> Result<Vec<LogEntry>, StoreError> {
|
||||
let path = self.log_path(id);
|
||||
if !path.exists() {
|
||||
return Err(StoreError::NotFound(id));
|
||||
@@ -79,7 +79,7 @@ impl Store for FsStore {
|
||||
Self::parse_jsonl(&content)
|
||||
}
|
||||
|
||||
fn list_sessions(&self) -> Result<Vec<SessionId>, StoreError> {
|
||||
fn list_segments(&self) -> Result<Vec<SegmentId>, StoreError> {
|
||||
let mut sessions = Vec::new();
|
||||
for entry in fs::read_dir(&self.root)? {
|
||||
let entry = entry?;
|
||||
@@ -88,7 +88,7 @@ impl Store for FsStore {
|
||||
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||
if name.ends_with(".jsonl") && !name.ends_with(".trace.jsonl") {
|
||||
let stem = name.trim_end_matches(".jsonl");
|
||||
if let Ok(id) = stem.parse::<SessionId>() {
|
||||
if let Ok(id) = stem.parse::<SegmentId>() {
|
||||
sessions.push(id);
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ impl Store for FsStore {
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
fn create_session(&self, id: SessionId, entries: &[LogEntry]) -> Result<(), StoreError> {
|
||||
fn create_segment(&self, id: SegmentId, entries: &[LogEntry]) -> Result<(), StoreError> {
|
||||
let path = self.log_path(id);
|
||||
let mut content = String::new();
|
||||
for entry in entries {
|
||||
@@ -109,11 +109,11 @@ impl Store for FsStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exists(&self, id: SessionId) -> Result<bool, StoreError> {
|
||||
fn exists(&self, id: SegmentId) -> Result<bool, StoreError> {
|
||||
Ok(self.log_path(id).exists())
|
||||
}
|
||||
|
||||
fn read_entry_count(&self, id: SessionId) -> Result<usize, StoreError> {
|
||||
fn read_entry_count(&self, id: SegmentId) -> Result<usize, StoreError> {
|
||||
let path = self.log_path(id);
|
||||
if !path.exists() {
|
||||
return Err(StoreError::NotFound(id));
|
||||
@@ -122,7 +122,7 @@ impl Store for FsStore {
|
||||
Ok(content.lines().filter(|l| !l.trim().is_empty()).count())
|
||||
}
|
||||
|
||||
fn append_trace(&self, id: SessionId, entry: &TraceEntry) -> Result<(), StoreError> {
|
||||
fn append_trace(&self, id: SegmentId, entry: &TraceEntry) -> Result<(), StoreError> {
|
||||
let line = serde_json::to_string(entry)?;
|
||||
self.append_line(&self.trace_path(id), &line)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Session persistence via append-only JSONL logs.
|
||||
//! Segment persistence via append-only JSONL logs.
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
@@ -11,15 +11,15 @@
|
||||
//! functions after state-mutating operations.
|
||||
//!
|
||||
//! Debug-mode [`TraceEntry`] records capture raw stream events in a separate
|
||||
//! `.trace.jsonl` file, independent of the session log.
|
||||
//! `.trace.jsonl` file, independent of the segment log.
|
||||
//!
|
||||
//! # Quick start
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use session_store::{create_session, restore, save_delta, FsStore, SessionStartState};
|
||||
//! use session_store::{create_segment, restore, save_delta, FsStore, SegmentStartState};
|
||||
//!
|
||||
//! let store = FsStore::new("./sessions")?;
|
||||
//! let session_id = create_session(&store, SessionStartState {
|
||||
//! let segment_id = create_segment(&store, SegmentStartState {
|
||||
//! system_prompt: None,
|
||||
//! config: &config,
|
||||
//! history: &[],
|
||||
@@ -29,8 +29,8 @@
|
||||
pub mod event_trace;
|
||||
pub mod fs_store;
|
||||
pub mod logged_item;
|
||||
pub mod session;
|
||||
pub mod session_log;
|
||||
pub mod segment;
|
||||
pub mod segment_log;
|
||||
pub mod store;
|
||||
pub mod system_item;
|
||||
|
||||
@@ -39,23 +39,23 @@ pub use fs_store::FsStore;
|
||||
pub use llm_worker::UsageRecord;
|
||||
pub use llm_worker::llm_client::types::{ContentPart, Item, Role};
|
||||
pub use logged_item::{LoggedContentPart, LoggedItem, LoggedRole, from_logged, to_logged};
|
||||
pub use session::{
|
||||
SessionStartState, append_entry, append_system_item, classify_history_item,
|
||||
create_compacted_session, create_session, create_session_with_id, ensure_head_or_fork, fork,
|
||||
pub use segment::{
|
||||
SegmentStartState, append_entry, append_system_item, classify_history_item,
|
||||
create_compacted_segment, create_segment, create_segment_with_id, ensure_head_or_fork, fork,
|
||||
fork_at, restore, save_config_changed, save_delta, save_extension, save_pod_scope,
|
||||
save_run_completed, save_run_errored, save_turn_end, save_usage, save_user_input,
|
||||
};
|
||||
pub use session_log::{
|
||||
LogEntry, POD_SCOPE_EXTENSION_DOMAIN, PodScopeSnapshot, RestoredState, SessionOrigin,
|
||||
pub use segment_log::{
|
||||
LogEntry, POD_SCOPE_EXTENSION_DOMAIN, PodScopeSnapshot, RestoredState, SegmentOrigin,
|
||||
collect_state,
|
||||
};
|
||||
pub use system_item::{SystemItem, render_pod_event};
|
||||
pub use store::{Store, StoreError};
|
||||
|
||||
/// Session identifier. UUID v7 (time-ordered, lexicographically sortable).
|
||||
pub type SessionId = uuid::Uuid;
|
||||
/// Segment identifier. UUID v7 (time-ordered, lexicographically sortable).
|
||||
pub type SegmentId = uuid::Uuid;
|
||||
|
||||
/// Generate a new session ID.
|
||||
pub fn new_session_id() -> SessionId {
|
||||
/// Generate a new segment ID.
|
||||
pub fn new_segment_id() -> SegmentId {
|
||||
uuid::Uuid::now_v7()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
//! Free functions for session persistence operations.
|
||||
//! Free functions for segment persistence operations.
|
||||
//!
|
||||
//! These functions record and restore session state without owning a Worker.
|
||||
//! These functions record and restore segment state without owning a Worker.
|
||||
//! The caller (typically Pod) holds the Worker directly and calls these
|
||||
//! functions after state-mutating operations.
|
||||
|
||||
use crate::SessionId;
|
||||
use crate::SegmentId;
|
||||
use crate::logged_item::{LoggedItem, to_logged};
|
||||
use crate::session_log::{self, LogEntry, PodScopeSnapshot, SessionOrigin};
|
||||
use crate::segment_log::{self, LogEntry, PodScopeSnapshot, SegmentOrigin};
|
||||
use crate::store::{Store, StoreError};
|
||||
use crate::system_item::SystemItem;
|
||||
use llm_worker::WorkerResult;
|
||||
@@ -14,108 +14,108 @@ use llm_worker::llm_client::RequestConfig;
|
||||
use llm_worker::llm_client::types::Item;
|
||||
use protocol::Segment;
|
||||
|
||||
/// State snapshot for creating a SessionStart entry.
|
||||
pub struct SessionStartState<'a> {
|
||||
/// State snapshot for creating a SegmentStart entry.
|
||||
pub struct SegmentStartState<'a> {
|
||||
pub system_prompt: Option<&'a str>,
|
||||
pub config: &'a RequestConfig,
|
||||
pub history: &'a [Item],
|
||||
}
|
||||
|
||||
/// Create a new session, writing the initial `SessionStart` entry.
|
||||
pub fn create_session(
|
||||
/// Create a new segment, writing the initial `SegmentStart` entry.
|
||||
pub fn create_segment(
|
||||
store: &impl Store,
|
||||
state: SessionStartState<'_>,
|
||||
) -> Result<SessionId, StoreError> {
|
||||
let session_id = crate::new_session_id();
|
||||
create_session_with_id(store, session_id, state)?;
|
||||
Ok(session_id)
|
||||
state: SegmentStartState<'_>,
|
||||
) -> Result<SegmentId, StoreError> {
|
||||
let segment_id = crate::new_segment_id();
|
||||
create_segment_with_id(store, segment_id, state)?;
|
||||
Ok(segment_id)
|
||||
}
|
||||
|
||||
/// Write a fresh `SessionStart` entry using a pre-generated session ID.
|
||||
/// Write a fresh `SegmentStart` entry using a pre-generated segment ID.
|
||||
///
|
||||
/// Used by callers that need to reserve a session ID synchronously but
|
||||
/// Used by callers that need to reserve a segment ID synchronously but
|
||||
/// defer the initial log append (e.g. Pod, which resolves a templated
|
||||
/// system prompt only at first turn).
|
||||
pub fn create_session_with_id(
|
||||
pub fn create_segment_with_id(
|
||||
store: &impl Store,
|
||||
session_id: SessionId,
|
||||
state: SessionStartState<'_>,
|
||||
segment_id: SegmentId,
|
||||
state: SegmentStartState<'_>,
|
||||
) -> Result<(), StoreError> {
|
||||
let entry = LogEntry::SessionStart {
|
||||
ts: session_log::now_millis(),
|
||||
let entry = LogEntry::SegmentStart {
|
||||
ts: segment_log::now_millis(),
|
||||
system_prompt: state.system_prompt.map(String::from),
|
||||
config: state.config.clone(),
|
||||
history: to_logged(state.history),
|
||||
forked_from: None,
|
||||
compacted_from: None,
|
||||
};
|
||||
store.append(session_id, &entry)
|
||||
store.append(segment_id, &entry)
|
||||
}
|
||||
|
||||
/// Create a compacted session from an existing one.
|
||||
/// Create a compacted segment from an existing one.
|
||||
///
|
||||
/// Records `compacted_from` provenance linking back to the source session
|
||||
/// Records `compacted_from` provenance linking back to the source segment
|
||||
/// at the turn boundary captured by `source_turn_count` (the most recent
|
||||
/// completed turn in the source).
|
||||
pub fn create_compacted_session(
|
||||
pub fn create_compacted_segment(
|
||||
store: &impl Store,
|
||||
state: SessionStartState<'_>,
|
||||
source_session_id: SessionId,
|
||||
state: SegmentStartState<'_>,
|
||||
source_session_id: SegmentId,
|
||||
source_turn_count: usize,
|
||||
) -> Result<SessionId, StoreError> {
|
||||
let session_id = crate::new_session_id();
|
||||
let entry = LogEntry::SessionStart {
|
||||
ts: session_log::now_millis(),
|
||||
) -> Result<SegmentId, StoreError> {
|
||||
let segment_id = crate::new_segment_id();
|
||||
let entry = LogEntry::SegmentStart {
|
||||
ts: segment_log::now_millis(),
|
||||
system_prompt: state.system_prompt.map(String::from),
|
||||
config: state.config.clone(),
|
||||
history: to_logged(state.history),
|
||||
forked_from: None,
|
||||
compacted_from: Some(SessionOrigin {
|
||||
session_id: source_session_id,
|
||||
compacted_from: Some(SegmentOrigin {
|
||||
segment_id: source_session_id,
|
||||
at_turn_index: source_turn_count,
|
||||
}),
|
||||
};
|
||||
store.append(session_id, &entry)?;
|
||||
Ok(session_id)
|
||||
store.append(segment_id, &entry)?;
|
||||
Ok(segment_id)
|
||||
}
|
||||
|
||||
/// Restore session state from a stored log.
|
||||
/// Restore segment state from a stored log.
|
||||
///
|
||||
/// Returns the reconstructed state. The caller is responsible for
|
||||
/// applying it to a Worker.
|
||||
pub fn restore(
|
||||
store: &impl Store,
|
||||
session_id: SessionId,
|
||||
) -> Result<crate::session_log::RestoredState, StoreError> {
|
||||
let entries = store.read_all(session_id)?;
|
||||
Ok(session_log::collect_state(&entries))
|
||||
segment_id: SegmentId,
|
||||
) -> Result<crate::segment_log::RestoredState, StoreError> {
|
||||
let entries = store.read_all(segment_id)?;
|
||||
Ok(segment_log::collect_state(&entries))
|
||||
}
|
||||
|
||||
/// Check if the store's entry count still matches the writer's tally.
|
||||
/// If not, auto-fork into a new session.
|
||||
/// If not, auto-fork into a new segment.
|
||||
///
|
||||
/// Updates `session_id` and `entries_written` in place when a fork occurs.
|
||||
/// Updates `segment_id` and `entries_written` in place when a fork occurs.
|
||||
pub fn ensure_head_or_fork(
|
||||
store: &impl Store,
|
||||
session_id: &mut SessionId,
|
||||
segment_id: &mut SegmentId,
|
||||
entries_written: &mut usize,
|
||||
state: SessionStartState<'_>,
|
||||
state: SegmentStartState<'_>,
|
||||
) -> Result<(), StoreError> {
|
||||
let store_count = store.read_entry_count(*session_id)?;
|
||||
let store_count = store.read_entry_count(*segment_id)?;
|
||||
if store_count == *entries_written {
|
||||
return Ok(());
|
||||
}
|
||||
let fork_id = crate::new_session_id();
|
||||
let entry = LogEntry::SessionStart {
|
||||
ts: session_log::now_millis(),
|
||||
let fork_id = crate::new_segment_id();
|
||||
let entry = LogEntry::SegmentStart {
|
||||
ts: segment_log::now_millis(),
|
||||
system_prompt: state.system_prompt.map(String::from),
|
||||
config: state.config.clone(),
|
||||
history: to_logged(state.history),
|
||||
forked_from: None,
|
||||
compacted_from: None,
|
||||
};
|
||||
store.create_session(fork_id, &[entry])?;
|
||||
*session_id = fork_id;
|
||||
store.create_segment(fork_id, &[entry])?;
|
||||
*segment_id = fork_id;
|
||||
*entries_written = 1;
|
||||
Ok(())
|
||||
}
|
||||
@@ -128,14 +128,14 @@ pub fn ensure_head_or_fork(
|
||||
/// [`Segment::flatten_to_text`].
|
||||
pub fn save_user_input(
|
||||
store: &impl Store,
|
||||
session_id: SessionId,
|
||||
segment_id: SegmentId,
|
||||
segments: Vec<Segment>,
|
||||
) -> Result<(), StoreError> {
|
||||
append_entry(
|
||||
store,
|
||||
session_id,
|
||||
segment_id,
|
||||
LogEntry::UserInput {
|
||||
ts: session_log::now_millis(),
|
||||
ts: segment_log::now_millis(),
|
||||
segments,
|
||||
},
|
||||
)
|
||||
@@ -151,21 +151,21 @@ pub fn save_user_input(
|
||||
/// `UserInput` entry.
|
||||
pub fn save_delta(
|
||||
store: &impl Store,
|
||||
session_id: SessionId,
|
||||
segment_id: SegmentId,
|
||||
new_items: &[Item],
|
||||
) -> Result<(), StoreError> {
|
||||
if new_items.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let ts = session_log::now_millis();
|
||||
let ts = segment_log::now_millis();
|
||||
for item in new_items {
|
||||
if item.is_user_message() {
|
||||
// Already persisted by save_user_input at submit time.
|
||||
continue;
|
||||
}
|
||||
let entry = classify_history_item(item, ts);
|
||||
append_entry(store, session_id, entry)?;
|
||||
append_entry(store, segment_id, entry)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -199,14 +199,14 @@ pub fn classify_history_item(item: &Item, ts: u64) -> LogEntry {
|
||||
/// commit shape used for assistant / tool result entries.
|
||||
pub fn append_system_item(
|
||||
store: &impl Store,
|
||||
session_id: SessionId,
|
||||
segment_id: SegmentId,
|
||||
item: SystemItem,
|
||||
) -> Result<(), StoreError> {
|
||||
append_entry(
|
||||
store,
|
||||
session_id,
|
||||
segment_id,
|
||||
LogEntry::SystemItem {
|
||||
ts: session_log::now_millis(),
|
||||
ts: segment_log::now_millis(),
|
||||
item,
|
||||
},
|
||||
)
|
||||
@@ -215,14 +215,14 @@ pub fn append_system_item(
|
||||
/// Log a TurnEnd entry.
|
||||
pub fn save_turn_end(
|
||||
store: &impl Store,
|
||||
session_id: SessionId,
|
||||
segment_id: SegmentId,
|
||||
turn_count: usize,
|
||||
) -> Result<(), StoreError> {
|
||||
append_entry(
|
||||
store,
|
||||
session_id,
|
||||
segment_id,
|
||||
LogEntry::TurnEnd {
|
||||
ts: session_log::now_millis(),
|
||||
ts: segment_log::now_millis(),
|
||||
turn_count,
|
||||
},
|
||||
)
|
||||
@@ -231,15 +231,15 @@ pub fn save_turn_end(
|
||||
/// Log a `RunCompleted` entry — `run()` / `resume()` returned `Ok(WorkerResult)`.
|
||||
pub fn save_run_completed(
|
||||
store: &impl Store,
|
||||
session_id: SessionId,
|
||||
segment_id: SegmentId,
|
||||
result: WorkerResult,
|
||||
interrupted: bool,
|
||||
) -> Result<(), StoreError> {
|
||||
append_entry(
|
||||
store,
|
||||
session_id,
|
||||
segment_id,
|
||||
LogEntry::RunCompleted {
|
||||
ts: session_log::now_millis(),
|
||||
ts: segment_log::now_millis(),
|
||||
interrupted,
|
||||
result,
|
||||
},
|
||||
@@ -252,15 +252,15 @@ pub fn save_run_completed(
|
||||
/// `to_string()` rendering as `message`.
|
||||
pub fn save_run_errored(
|
||||
store: &impl Store,
|
||||
session_id: SessionId,
|
||||
segment_id: SegmentId,
|
||||
message: String,
|
||||
interrupted: bool,
|
||||
) -> Result<(), StoreError> {
|
||||
append_entry(
|
||||
store,
|
||||
session_id,
|
||||
segment_id,
|
||||
LogEntry::RunErrored {
|
||||
ts: session_log::now_millis(),
|
||||
ts: segment_log::now_millis(),
|
||||
interrupted,
|
||||
message,
|
||||
},
|
||||
@@ -275,7 +275,7 @@ pub fn save_run_errored(
|
||||
/// 済ませた値を渡す。
|
||||
pub fn save_usage(
|
||||
store: &impl Store,
|
||||
session_id: SessionId,
|
||||
segment_id: SegmentId,
|
||||
history_len: usize,
|
||||
input_total_tokens: u64,
|
||||
cache_read_tokens: u64,
|
||||
@@ -284,9 +284,9 @@ pub fn save_usage(
|
||||
) -> Result<(), StoreError> {
|
||||
append_entry(
|
||||
store,
|
||||
session_id,
|
||||
segment_id,
|
||||
LogEntry::LlmUsage {
|
||||
ts: session_log::now_millis(),
|
||||
ts: segment_log::now_millis(),
|
||||
history_len,
|
||||
input_total_tokens,
|
||||
cache_read_tokens,
|
||||
@@ -303,15 +303,15 @@ pub fn save_usage(
|
||||
/// Use `RestoredState.extensions` to read entries back at restore time.
|
||||
pub fn save_extension(
|
||||
store: &impl Store,
|
||||
session_id: SessionId,
|
||||
segment_id: SegmentId,
|
||||
domain: impl Into<String>,
|
||||
payload: serde_json::Value,
|
||||
) -> Result<(), StoreError> {
|
||||
append_entry(
|
||||
store,
|
||||
session_id,
|
||||
segment_id,
|
||||
LogEntry::Extension {
|
||||
ts: session_log::now_millis(),
|
||||
ts: segment_log::now_millis(),
|
||||
domain: domain.into(),
|
||||
payload,
|
||||
},
|
||||
@@ -321,14 +321,14 @@ pub fn save_extension(
|
||||
/// Log the Pod's latest runtime scope snapshot.
|
||||
pub fn save_pod_scope(
|
||||
store: &impl Store,
|
||||
session_id: SessionId,
|
||||
segment_id: SegmentId,
|
||||
snapshot: &PodScopeSnapshot,
|
||||
) -> Result<(), StoreError> {
|
||||
let payload = serde_json::to_value(snapshot)?;
|
||||
save_extension(
|
||||
store,
|
||||
session_id,
|
||||
session_log::POD_SCOPE_EXTENSION_DOMAIN,
|
||||
segment_id,
|
||||
segment_log::POD_SCOPE_EXTENSION_DOMAIN,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
@@ -336,35 +336,35 @@ pub fn save_pod_scope(
|
||||
/// Log a `ConfigChanged` entry.
|
||||
pub fn save_config_changed(
|
||||
store: &impl Store,
|
||||
session_id: SessionId,
|
||||
segment_id: SegmentId,
|
||||
config: &RequestConfig,
|
||||
) -> Result<(), StoreError> {
|
||||
append_entry(
|
||||
store,
|
||||
session_id,
|
||||
segment_id,
|
||||
LogEntry::ConfigChanged {
|
||||
ts: session_log::now_millis(),
|
||||
ts: segment_log::now_millis(),
|
||||
config: config.clone(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Fork the current state into a new session.
|
||||
pub fn fork(store: &impl Store, state: SessionStartState<'_>) -> Result<SessionId, StoreError> {
|
||||
let fork_id = crate::new_session_id();
|
||||
let entry = LogEntry::SessionStart {
|
||||
ts: session_log::now_millis(),
|
||||
/// Fork the current state into a new segment.
|
||||
pub fn fork(store: &impl Store, state: SegmentStartState<'_>) -> Result<SegmentId, StoreError> {
|
||||
let fork_id = crate::new_segment_id();
|
||||
let entry = LogEntry::SegmentStart {
|
||||
ts: segment_log::now_millis(),
|
||||
system_prompt: state.system_prompt.map(String::from),
|
||||
config: state.config.clone(),
|
||||
history: to_logged(state.history),
|
||||
forked_from: None,
|
||||
compacted_from: None,
|
||||
};
|
||||
store.create_session(fork_id, &[entry])?;
|
||||
store.create_segment(fork_id, &[entry])?;
|
||||
Ok(fork_id)
|
||||
}
|
||||
|
||||
/// Fork from a turn boundary in a stored session's log.
|
||||
/// Fork from a turn boundary in a stored segment log.
|
||||
///
|
||||
/// `at_turn_index` is the `turn_count` of the most recent completed
|
||||
/// `TurnEnd` in the source segment that the fork should branch from.
|
||||
@@ -372,16 +372,16 @@ pub fn fork(store: &impl Store, state: SessionStartState<'_>) -> Result<SessionI
|
||||
/// after it are not carried into the new segment.
|
||||
pub fn fork_at(
|
||||
store: &impl Store,
|
||||
source_id: SessionId,
|
||||
source_id: SegmentId,
|
||||
at_turn_index: usize,
|
||||
) -> Result<SessionId, StoreError> {
|
||||
) -> Result<SegmentId, StoreError> {
|
||||
let entries = store.read_all(source_id)?;
|
||||
let cut = if at_turn_index == 0 {
|
||||
// Branch directly after the SessionStart (or whatever opens the
|
||||
// Branch directly after the SegmentStart (or whatever opens the
|
||||
// segment), before any turn completes.
|
||||
entries
|
||||
.iter()
|
||||
.position(|e| !matches!(e, LogEntry::SessionStart { .. }))
|
||||
.position(|e| !matches!(e, LogEntry::SegmentStart { .. }))
|
||||
.unwrap_or(entries.len())
|
||||
} else {
|
||||
entries
|
||||
@@ -390,21 +390,21 @@ pub fn fork_at(
|
||||
.map(|i| i + 1)
|
||||
.unwrap_or(entries.len())
|
||||
};
|
||||
let state = session_log::collect_state(&entries[..cut]);
|
||||
let state = segment_log::collect_state(&entries[..cut]);
|
||||
|
||||
let fork_id = crate::new_session_id();
|
||||
let entry = LogEntry::SessionStart {
|
||||
ts: session_log::now_millis(),
|
||||
let fork_id = crate::new_segment_id();
|
||||
let entry = LogEntry::SegmentStart {
|
||||
ts: segment_log::now_millis(),
|
||||
system_prompt: state.system_prompt,
|
||||
config: state.config,
|
||||
history: to_logged(&state.history),
|
||||
forked_from: Some(SessionOrigin {
|
||||
session_id: source_id,
|
||||
forked_from: Some(SegmentOrigin {
|
||||
segment_id: source_id,
|
||||
at_turn_index,
|
||||
}),
|
||||
compacted_from: None,
|
||||
};
|
||||
store.create_session(fork_id, &[entry])?;
|
||||
store.create_segment(fork_id, &[entry])?;
|
||||
Ok(fork_id)
|
||||
}
|
||||
|
||||
@@ -415,8 +415,8 @@ pub fn fork_at(
|
||||
/// it needs the same value for an in-memory mirror + broadcast).
|
||||
pub fn append_entry(
|
||||
store: &impl Store,
|
||||
session_id: SessionId,
|
||||
segment_id: SegmentId,
|
||||
entry: LogEntry,
|
||||
) -> Result<(), StoreError> {
|
||||
store.append(session_id, &entry)
|
||||
store.append(segment_id, &entry)
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
//! Session log types for append-only JSONL persistence.
|
||||
//! Segment log types for append-only JSONL persistence.
|
||||
//!
|
||||
//! Each [`LogEntry`] represents a single state transition in a session,
|
||||
//! serialized as one line in a `.jsonl` file. Reading all entries and
|
||||
//! collecting them via [`collect_state`] reconstructs the full [`Worker`] state.
|
||||
//! Each [`LogEntry`] represents a single state transition within one
|
||||
//! segment, serialized as one line in a `.jsonl` file. Reading all
|
||||
//! entries and collecting them via [`collect_state`] reconstructs the
|
||||
//! full [`Worker`] state at that segment.
|
||||
//!
|
||||
//! The on-disk format is one `LogEntry` per line — entries are positionally
|
||||
//! ordered. Fork lineage references between segments use turn-number indices
|
||||
//! (`SessionOrigin.at_turn_index`) rather than per-entry hashes.
|
||||
//! (`SegmentOrigin.at_turn_index`) rather than per-entry hashes.
|
||||
|
||||
use llm_worker::llm_client::types::{Item, RequestConfig};
|
||||
use llm_worker::{UsageRecord, WorkerResult};
|
||||
@@ -16,10 +17,10 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::logged_item::LoggedItem;
|
||||
use crate::system_item::SystemItem;
|
||||
|
||||
/// A single session log entry, serialized as one JSONL line.
|
||||
/// A single segment log entry, serialized as one JSONL line.
|
||||
///
|
||||
/// Variants correspond to specific mutation points in `Worker`:
|
||||
/// - `SessionStart` — always the first entry; captures initial state
|
||||
/// - `SegmentStart` — always the first entry; captures initial state
|
||||
/// - `Invoke` — IDLE → active marker (start of a new self-driving cycle)
|
||||
/// - `UserInput` / `AssistantItems` / `ToolResults` / `HookInjectedItems` — history appends
|
||||
/// - `TurnEnd` — AgentTurn boundary marker; carries the post-increment
|
||||
@@ -32,19 +33,19 @@ use crate::system_item::SystemItem;
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum LogEntry {
|
||||
/// Session start. Always the first entry in a log.
|
||||
/// For forked sessions, `history` contains the seed state from the parent.
|
||||
SessionStart {
|
||||
/// Segment start. Always the first entry in a segment log.
|
||||
/// For forked segments, `history` contains the seed state from the parent.
|
||||
SegmentStart {
|
||||
ts: u64,
|
||||
system_prompt: Option<String>,
|
||||
config: RequestConfig,
|
||||
history: Vec<LoggedItem>,
|
||||
/// Origin: forked from another session at a specific turn boundary.
|
||||
/// Origin: forked from another segment at a specific turn boundary.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
forked_from: Option<SessionOrigin>,
|
||||
/// Origin: compacted from another session at a specific turn boundary.
|
||||
forked_from: Option<SegmentOrigin>,
|
||||
/// Origin: compacted from another segment at a specific turn boundary.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
compacted_from: Option<SessionOrigin>,
|
||||
compacted_from: Option<SegmentOrigin>,
|
||||
},
|
||||
|
||||
/// IDLE → active marker. Records the start of a new self-driving
|
||||
@@ -66,7 +67,7 @@ pub enum LogEntry {
|
||||
|
||||
/// User input accepted at submit time. Carries the original typed
|
||||
/// `Vec<Segment>` so clients can re-render typed atoms (paste chips,
|
||||
/// file/knowledge refs, workflow invocations) on session restore.
|
||||
/// file/knowledge refs, workflow invocations) on segment restore.
|
||||
/// Replay flattens these into a `Item::user_message` for the worker
|
||||
/// history; the worker layer never sees segments directly.
|
||||
UserInput { ts: u64, segments: Vec<Segment> },
|
||||
@@ -87,7 +88,7 @@ pub enum LogEntry {
|
||||
/// dispatch on `kind` for typed rendering.
|
||||
SystemItem { ts: u64, item: SystemItem },
|
||||
|
||||
/// Legacy plural form: kept **read-only** so old session logs still
|
||||
/// Legacy plural form: kept **read-only** so old segment logs still
|
||||
/// open. New writes always use the singular `AssistantItem`. Items
|
||||
/// are flattened on replay.
|
||||
AssistantItems { ts: u64, items: Vec<LoggedItem> },
|
||||
@@ -169,10 +170,10 @@ pub enum LogEntry {
|
||||
/// `at_turn_index` is the `turn_count` value of the most recent
|
||||
/// `TurnEnd` entry preceding the split point in the source segment.
|
||||
/// A value of `0` means the split happened before any turn completed
|
||||
/// (e.g. immediately after `SessionStart`).
|
||||
/// (e.g. immediately after `SegmentStart`).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct SessionOrigin {
|
||||
pub session_id: crate::SessionId,
|
||||
pub struct SegmentOrigin {
|
||||
pub segment_id: crate::SegmentId,
|
||||
pub at_turn_index: usize,
|
||||
}
|
||||
|
||||
@@ -194,7 +195,7 @@ pub struct RestoredState {
|
||||
pub history: Vec<Item>,
|
||||
pub turn_count: usize,
|
||||
pub last_run_interrupted: bool,
|
||||
/// Number of entries replayed. `0` means the session log was empty.
|
||||
/// Number of entries replayed. `0` means the segment log was empty.
|
||||
/// Writers track their own append count via the same counter so
|
||||
/// `ensure_head_or_fork` can compare it with the on-disk count.
|
||||
pub entries_count: usize,
|
||||
@@ -206,14 +207,14 @@ pub struct RestoredState {
|
||||
/// session-store は domain を不透明扱いし、各ドメインが自前で fold する。
|
||||
pub extensions: Vec<(String, serde_json::Value)>,
|
||||
/// Latest runtime scope snapshot persisted by the Pod. `None` means
|
||||
/// the session predates scope persistence or the payload was corrupt.
|
||||
/// the segment predates scope persistence or the payload was corrupt.
|
||||
pub pod_scope: Option<PodScopeSnapshot>,
|
||||
/// User submissions in original typed form, in submit order.
|
||||
/// One entry per `LogEntry::UserInput`; the K-th entry corresponds to
|
||||
/// the K-th `Item::user_message` derived during replay (modulo
|
||||
/// pre-compaction history seeded via `SessionStart.history`, whose
|
||||
/// pre-compaction history seeded via `SegmentStart.history`, whose
|
||||
/// original segments are not preserved). Used by clients to re-render
|
||||
/// typed atoms (paste chips, refs) on session restore.
|
||||
/// typed atoms (paste chips, refs) on segment restore.
|
||||
pub user_segments: Vec<Vec<Segment>>,
|
||||
}
|
||||
|
||||
@@ -236,7 +237,7 @@ pub fn collect_state(entries: &[LogEntry]) -> RestoredState {
|
||||
state.entries_count += 1;
|
||||
|
||||
match entry {
|
||||
LogEntry::SessionStart {
|
||||
LogEntry::SegmentStart {
|
||||
system_prompt,
|
||||
config,
|
||||
history,
|
||||
@@ -316,7 +317,7 @@ pub fn collect_state(entries: &[LogEntry]) -> RestoredState {
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"discarding malformed pod.scope snapshot from session log"
|
||||
"discarding malformed pod.scope snapshot from segment log"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -350,8 +351,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_session_start_sets_initial_state() {
|
||||
let state = collect_state(&[LogEntry::SessionStart {
|
||||
fn replay_segment_start_sets_initial_state() {
|
||||
let state = collect_state(&[LogEntry::SegmentStart {
|
||||
ts: 1000,
|
||||
system_prompt: Some("You are helpful.".into()),
|
||||
config: RequestConfig::default().with_max_tokens(1024),
|
||||
@@ -368,7 +369,7 @@ mod tests {
|
||||
#[test]
|
||||
fn replay_full_turn() {
|
||||
let state = collect_state(&[
|
||||
LogEntry::SessionStart {
|
||||
LogEntry::SegmentStart {
|
||||
ts: 1000,
|
||||
system_prompt: None,
|
||||
config: RequestConfig::default(),
|
||||
@@ -402,7 +403,7 @@ mod tests {
|
||||
#[test]
|
||||
fn replay_with_tool_calls() {
|
||||
let state = collect_state(&[
|
||||
LogEntry::SessionStart {
|
||||
LogEntry::SegmentStart {
|
||||
ts: 1000,
|
||||
system_prompt: None,
|
||||
config: RequestConfig::default(),
|
||||
@@ -439,7 +440,7 @@ mod tests {
|
||||
#[test]
|
||||
fn replay_config_changed() {
|
||||
let state = collect_state(&[
|
||||
LogEntry::SessionStart {
|
||||
LogEntry::SegmentStart {
|
||||
ts: 1000,
|
||||
system_prompt: None,
|
||||
config: RequestConfig::default(),
|
||||
@@ -458,7 +459,7 @@ mod tests {
|
||||
#[test]
|
||||
fn replay_llm_usage_appends_to_usage_history() {
|
||||
let state = collect_state(&[
|
||||
LogEntry::SessionStart {
|
||||
LogEntry::SegmentStart {
|
||||
ts: 1000,
|
||||
system_prompt: None,
|
||||
config: RequestConfig::default(),
|
||||
@@ -504,7 +505,7 @@ mod tests {
|
||||
#[test]
|
||||
fn replay_without_llm_usage_keeps_usage_history_empty() {
|
||||
let state = collect_state(&[
|
||||
LogEntry::SessionStart {
|
||||
LogEntry::SegmentStart {
|
||||
ts: 1000,
|
||||
system_prompt: None,
|
||||
config: RequestConfig::default(),
|
||||
@@ -575,7 +576,7 @@ mod tests {
|
||||
#[test]
|
||||
fn replay_invoke_marker_does_not_mutate_state() {
|
||||
let state = collect_state(&[
|
||||
LogEntry::SessionStart {
|
||||
LogEntry::SegmentStart {
|
||||
ts: 0,
|
||||
system_prompt: None,
|
||||
config: RequestConfig::default(),
|
||||
@@ -607,7 +608,7 @@ mod tests {
|
||||
#[test]
|
||||
fn replay_extension_collects_domain_payload_pairs() {
|
||||
let state = collect_state(&[
|
||||
LogEntry::SessionStart {
|
||||
LogEntry::SegmentStart {
|
||||
ts: 1000,
|
||||
system_prompt: None,
|
||||
config: RequestConfig::default(),
|
||||
@@ -690,7 +691,7 @@ mod tests {
|
||||
let json = serde_json::to_string(&entry).unwrap();
|
||||
let parsed: LogEntry = serde_json::from_str(&json).unwrap();
|
||||
let state = collect_state(&[
|
||||
LogEntry::SessionStart {
|
||||
LogEntry::SegmentStart {
|
||||
ts: 1,
|
||||
system_prompt: None,
|
||||
config: RequestConfig::default(),
|
||||
@@ -1,18 +1,18 @@
|
||||
//! Persistence backend abstraction.
|
||||
//!
|
||||
//! [`Store`] defines the sync interface for reading and writing session logs.
|
||||
//! [`Store`] defines the sync interface for reading and writing segment logs.
|
||||
//! Implementations handle the physical storage (filesystem, database, etc.).
|
||||
//!
|
||||
//! Sync (rather than async) is intentional: a session log append is a single
|
||||
//! Sync (rather than async) is intentional: a segment log append is a single
|
||||
//! `< 1 KiB` line on local fs and completes well below a millisecond. Going
|
||||
//! through `tokio::fs` would force every caller — including `Worker`'s sync
|
||||
//! `on_history_append` callback — to bridge sync → async via a channel +
|
||||
//! drain task. Keeping the store sync lets the worker callback, Pod commit
|
||||
//! paths, and `PodInterceptor` all share one direct `append_entry` call.
|
||||
|
||||
use crate::SessionId;
|
||||
use crate::SegmentId;
|
||||
use crate::event_trace::TraceEntry;
|
||||
use crate::session_log::LogEntry;
|
||||
use crate::segment_log::LogEntry;
|
||||
|
||||
/// Errors from the persistence store.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
@@ -23,43 +23,43 @@ pub enum StoreError {
|
||||
#[error("serialization error: {0}")]
|
||||
Serde(#[from] serde_json::Error),
|
||||
|
||||
#[error("session not found: {0}")]
|
||||
NotFound(SessionId),
|
||||
#[error("segment not found: {0}")]
|
||||
NotFound(SegmentId),
|
||||
|
||||
#[error("log corrupted at line {line}: {message}")]
|
||||
Corrupt { line: usize, message: String },
|
||||
}
|
||||
|
||||
/// Sync persistence backend for session logs.
|
||||
/// Sync persistence backend for segment logs.
|
||||
///
|
||||
/// All methods take `&self` — implementations should use interior mutability
|
||||
/// (e.g., append-mode file handles) when needed.
|
||||
pub trait Store: Send + Sync {
|
||||
/// Append a single log entry to the session log.
|
||||
/// Append a single log entry to the segment log.
|
||||
///
|
||||
/// One line per call. The kernel orders concurrent `O_APPEND` writes
|
||||
/// for lines < `PIPE_BUF`, so user-space serialization is unnecessary.
|
||||
fn append(&self, id: SessionId, entry: &LogEntry) -> Result<(), StoreError>;
|
||||
fn append(&self, id: SegmentId, entry: &LogEntry) -> Result<(), StoreError>;
|
||||
|
||||
/// Read all log entries for a session, in order.
|
||||
fn read_all(&self, id: SessionId) -> Result<Vec<LogEntry>, StoreError>;
|
||||
/// Read all log entries for a segment, in order.
|
||||
fn read_all(&self, id: SegmentId) -> Result<Vec<LogEntry>, StoreError>;
|
||||
|
||||
/// List all session IDs, most recent first.
|
||||
fn list_sessions(&self) -> Result<Vec<SessionId>, StoreError>;
|
||||
/// List all segment IDs, most recent first.
|
||||
fn list_segments(&self) -> Result<Vec<SegmentId>, StoreError>;
|
||||
|
||||
/// Create a new session with initial entries.
|
||||
fn create_session(&self, id: SessionId, entries: &[LogEntry]) -> Result<(), StoreError>;
|
||||
/// Create a new segment with initial entries.
|
||||
fn create_segment(&self, id: SegmentId, entries: &[LogEntry]) -> Result<(), StoreError>;
|
||||
|
||||
/// Check if a session exists.
|
||||
fn exists(&self, id: SessionId) -> Result<bool, StoreError>;
|
||||
/// Check if a segment exists.
|
||||
fn exists(&self, id: SegmentId) -> Result<bool, StoreError>;
|
||||
|
||||
/// Count entries currently stored for a session.
|
||||
/// Count entries currently stored for a segment.
|
||||
///
|
||||
/// Used by `ensure_head_or_fork` to detect concurrent writers:
|
||||
/// if the on-disk count exceeds the writer's own append tally,
|
||||
/// another process has extended the log.
|
||||
fn read_entry_count(&self, id: SessionId) -> Result<usize, StoreError>;
|
||||
fn read_entry_count(&self, id: SegmentId) -> Result<usize, StoreError>;
|
||||
|
||||
/// Append a trace entry to the debug event trace file.
|
||||
fn append_trace(&self, id: SessionId, entry: &TraceEntry) -> Result<(), StoreError>;
|
||||
fn append_trace(&self, id: SegmentId, entry: &TraceEntry) -> Result<(), StoreError>;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ use serde::{Deserialize, Serialize};
|
||||
/// path / knowledge slug / workflow slug / etc.), plus a pre-rendered
|
||||
/// `body` (where applicable) that is the exact `role:system` text the
|
||||
/// LLM actually saw at commit time. `body` is denormalised so that
|
||||
/// session log replay reconstructs worker history byte-identical to
|
||||
/// segment log replay reconstructs worker history byte-identical to
|
||||
/// what was on the wire — even when prompt overrides (e.g. custom
|
||||
/// `notify_wrapper` template) re-shape the live rendering on a later
|
||||
/// resume.
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use llm_worker::WorkerResult;
|
||||
use llm_worker::llm_client::types::{Item, RequestConfig};
|
||||
use session_store::{FsStore, LogEntry, Store, TraceEntry, collect_state, new_session_id};
|
||||
use session_store::{FsStore, LogEntry, Store, TraceEntry, collect_state, new_segment_id};
|
||||
|
||||
#[test]
|
||||
fn round_trip_write_and_read() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = FsStore::new(dir.path()).unwrap();
|
||||
let id = new_session_id();
|
||||
let id = new_segment_id();
|
||||
|
||||
let entries = vec![
|
||||
LogEntry::SessionStart {
|
||||
LogEntry::SegmentStart {
|
||||
ts: 1000,
|
||||
system_prompt: Some("You are helpful.".into()),
|
||||
config: RequestConfig::default().with_max_tokens(1024),
|
||||
@@ -56,9 +56,9 @@ fn round_trip_write_and_read() {
|
||||
fn create_session_writes_all_entries() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = FsStore::new(dir.path()).unwrap();
|
||||
let id = new_session_id();
|
||||
let id = new_segment_id();
|
||||
|
||||
let entries = [LogEntry::SessionStart {
|
||||
let entries = [LogEntry::SegmentStart {
|
||||
ts: 1000,
|
||||
system_prompt: None,
|
||||
config: RequestConfig::default(),
|
||||
@@ -70,7 +70,7 @@ fn create_session_writes_all_entries() {
|
||||
compacted_from: None,
|
||||
}];
|
||||
|
||||
store.create_session(id, &entries).unwrap();
|
||||
store.create_segment(id, &entries).unwrap();
|
||||
let read_back = store.read_all(id).unwrap();
|
||||
assert_eq!(read_back.len(), 1);
|
||||
|
||||
@@ -83,12 +83,12 @@ fn list_sessions_returns_newest_first() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = FsStore::new(dir.path()).unwrap();
|
||||
|
||||
let id1 = new_session_id();
|
||||
let id1 = new_segment_id();
|
||||
// Small delay to ensure different UUID v7 timestamps
|
||||
std::thread::sleep(std::time::Duration::from_millis(2));
|
||||
let id2 = new_session_id();
|
||||
let id2 = new_segment_id();
|
||||
|
||||
let entry = LogEntry::SessionStart {
|
||||
let entry = LogEntry::SegmentStart {
|
||||
ts: 1000,
|
||||
system_prompt: None,
|
||||
config: RequestConfig::default(),
|
||||
@@ -100,7 +100,7 @@ fn list_sessions_returns_newest_first() {
|
||||
store.append(id1, &entry).unwrap();
|
||||
store.append(id2, &entry).unwrap();
|
||||
|
||||
let sessions = store.list_sessions().unwrap();
|
||||
let sessions = store.list_segments().unwrap();
|
||||
assert_eq!(sessions.len(), 2);
|
||||
assert_eq!(sessions[0], id2); // newest first
|
||||
assert_eq!(sessions[1], id1);
|
||||
@@ -110,14 +110,14 @@ fn list_sessions_returns_newest_first() {
|
||||
fn exists_returns_correct_state() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = FsStore::new(dir.path()).unwrap();
|
||||
let id = new_session_id();
|
||||
let id = new_segment_id();
|
||||
|
||||
assert!(!store.exists(id).unwrap());
|
||||
|
||||
store
|
||||
.append(
|
||||
id,
|
||||
&LogEntry::SessionStart {
|
||||
&LogEntry::SegmentStart {
|
||||
ts: 1000,
|
||||
system_prompt: None,
|
||||
config: RequestConfig::default(),
|
||||
@@ -135,7 +135,7 @@ fn exists_returns_correct_state() {
|
||||
fn not_found_error_for_missing_session() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = FsStore::new(dir.path()).unwrap();
|
||||
let id = new_session_id();
|
||||
let id = new_segment_id();
|
||||
|
||||
let result = store.read_all(id);
|
||||
assert!(result.is_err());
|
||||
@@ -145,12 +145,12 @@ fn not_found_error_for_missing_session() {
|
||||
fn trace_entries_in_separate_file() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = FsStore::new(dir.path()).unwrap();
|
||||
let id = new_session_id();
|
||||
let id = new_segment_id();
|
||||
|
||||
store
|
||||
.append(
|
||||
id,
|
||||
&LogEntry::SessionStart {
|
||||
&LogEntry::SegmentStart {
|
||||
ts: 1000,
|
||||
system_prompt: None,
|
||||
config: RequestConfig::default(),
|
||||
@@ -183,10 +183,10 @@ fn trace_entries_in_separate_file() {
|
||||
fn read_entry_count_matches_append_tally() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = FsStore::new(dir.path()).unwrap();
|
||||
let id = new_session_id();
|
||||
let id = new_segment_id();
|
||||
|
||||
let entries = [
|
||||
LogEntry::SessionStart {
|
||||
LogEntry::SegmentStart {
|
||||
ts: 1000,
|
||||
system_prompt: None,
|
||||
config: RequestConfig::default(),
|
||||
|
||||
@@ -9,7 +9,7 @@ use llm_worker::interceptor::{Interceptor, TurnEndAction};
|
||||
use llm_worker::llm_client::event::{Event, ResponseStatus, StatusEvent};
|
||||
use llm_worker::llm_client::types::{Item, RequestConfig};
|
||||
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
||||
use session_store::{FsStore, LogEntry, SessionStartState, Store, collect_state};
|
||||
use session_store::{FsStore, LogEntry, SegmentStartState, Store, collect_state};
|
||||
|
||||
// =============================================================================
|
||||
// Helpers
|
||||
@@ -95,13 +95,13 @@ fn make_store() -> (tempfile::TempDir, FsStore) {
|
||||
async fn run_and_persist(
|
||||
worker: Worker<MockLlmClient>,
|
||||
store: &FsStore,
|
||||
session_id: session_store::SessionId,
|
||||
segment_id: session_store::SegmentId,
|
||||
input: &str,
|
||||
) -> (Worker<MockLlmClient>, llm_worker::WorkerResult) {
|
||||
// Mirror Pod's run-entry contract: log the user input as segments
|
||||
// before the worker pushes its flattened user_message; save_delta
|
||||
// skips the resulting user_message item to avoid double-write.
|
||||
session_store::save_user_input(store, session_id, vec![protocol::Segment::text(input)])
|
||||
session_store::save_user_input(store, segment_id, vec![protocol::Segment::text(input)])
|
||||
.unwrap();
|
||||
|
||||
let history_before = worker.history().len();
|
||||
@@ -111,14 +111,14 @@ async fn run_and_persist(
|
||||
let worker = locked.unlock();
|
||||
|
||||
let new_items = &worker.history()[history_before..];
|
||||
session_store::save_delta(store, session_id, new_items).unwrap();
|
||||
session_store::save_turn_end(store, session_id, worker.turn_count()).unwrap();
|
||||
session_store::save_delta(store, segment_id, new_items).unwrap();
|
||||
session_store::save_turn_end(store, segment_id, worker.turn_count()).unwrap();
|
||||
|
||||
match &result {
|
||||
Ok(r) => {
|
||||
session_store::save_run_completed(
|
||||
store,
|
||||
session_id,
|
||||
segment_id,
|
||||
r.clone(),
|
||||
worker.last_run_interrupted(),
|
||||
)
|
||||
@@ -127,7 +127,7 @@ async fn run_and_persist(
|
||||
Err(e) => {
|
||||
session_store::save_run_errored(
|
||||
store,
|
||||
session_id,
|
||||
segment_id,
|
||||
e.to_string(),
|
||||
worker.last_run_interrupted(),
|
||||
)
|
||||
@@ -149,9 +149,9 @@ async fn session_run_logs_entries() {
|
||||
let client = MockLlmClient::new(simple_text_events());
|
||||
let worker = Worker::new(client);
|
||||
|
||||
let sid = session_store::create_session(
|
||||
let sid = session_store::create_segment(
|
||||
&store,
|
||||
SessionStartState {
|
||||
SegmentStartState {
|
||||
system_prompt: worker.get_system_prompt(),
|
||||
config: worker.request_config(),
|
||||
history: worker.history(),
|
||||
@@ -164,15 +164,15 @@ async fn session_run_logs_entries() {
|
||||
|
||||
let entries = store.read_all(sid).unwrap();
|
||||
|
||||
// SessionStart, UserInput, AssistantItems, TurnEnd, RunCompleted (at minimum)
|
||||
// SegmentStart, UserInput, AssistantItems, TurnEnd, RunCompleted (at minimum)
|
||||
assert!(
|
||||
entries.len() >= 4,
|
||||
"expected at least 4 entries, got {}",
|
||||
entries.len()
|
||||
);
|
||||
|
||||
// First entry is SessionStart
|
||||
assert!(matches!(&entries[0], LogEntry::SessionStart { .. }));
|
||||
// First entry is SegmentStart
|
||||
assert!(matches!(&entries[0], LogEntry::SegmentStart { .. }));
|
||||
|
||||
// Has a RunCompleted with Finished
|
||||
let has_finished = entries.iter().any(|e| {
|
||||
@@ -194,9 +194,9 @@ async fn session_restore_round_trip() {
|
||||
let mut worker = Worker::new(client);
|
||||
worker.set_system_prompt("You are helpful.");
|
||||
|
||||
let sid = session_store::create_session(
|
||||
let sid = session_store::create_segment(
|
||||
&store,
|
||||
SessionStartState {
|
||||
SegmentStartState {
|
||||
system_prompt: worker.get_system_prompt(),
|
||||
config: worker.request_config(),
|
||||
history: worker.history(),
|
||||
@@ -225,9 +225,9 @@ async fn session_run_with_tool_call() {
|
||||
let mut worker = Worker::new(client);
|
||||
worker.register_tool(weather_tool_definition());
|
||||
|
||||
let sid = session_store::create_session(
|
||||
let sid = session_store::create_segment(
|
||||
&store,
|
||||
SessionStartState {
|
||||
SegmentStartState {
|
||||
system_prompt: worker.get_system_prompt(),
|
||||
config: worker.request_config(),
|
||||
history: worker.history(),
|
||||
@@ -260,9 +260,9 @@ async fn session_resume_after_pause() {
|
||||
worker.register_tool(weather_tool_definition());
|
||||
worker.set_interceptor(PausePolicy);
|
||||
|
||||
let sid = session_store::create_session(
|
||||
let sid = session_store::create_segment(
|
||||
&store,
|
||||
SessionStartState {
|
||||
SegmentStartState {
|
||||
system_prompt: worker.get_system_prompt(),
|
||||
config: worker.request_config(),
|
||||
history: worker.history(),
|
||||
@@ -298,9 +298,9 @@ async fn session_fork_preserves_state() {
|
||||
let mut worker = Worker::new(client);
|
||||
worker.set_system_prompt("System prompt");
|
||||
|
||||
let sid = session_store::create_session(
|
||||
let sid = session_store::create_segment(
|
||||
&store,
|
||||
SessionStartState {
|
||||
SegmentStartState {
|
||||
system_prompt: worker.get_system_prompt(),
|
||||
config: worker.request_config(),
|
||||
history: worker.history(),
|
||||
@@ -313,7 +313,7 @@ async fn session_fork_preserves_state() {
|
||||
let original_history_len = worker.history().len();
|
||||
let fork_id = session_store::fork(
|
||||
&store,
|
||||
SessionStartState {
|
||||
SegmentStartState {
|
||||
system_prompt: worker.get_system_prompt(),
|
||||
config: worker.request_config(),
|
||||
history: worker.history(),
|
||||
@@ -321,10 +321,10 @@ async fn session_fork_preserves_state() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Fork should have a SessionStart with the current history
|
||||
// Fork should have a SegmentStart with the current history
|
||||
let fork_entries = store.read_all(fork_id).unwrap();
|
||||
assert_eq!(fork_entries.len(), 1);
|
||||
assert!(matches!(&fork_entries[0], LogEntry::SessionStart { .. }));
|
||||
assert!(matches!(&fork_entries[0], LogEntry::SegmentStart { .. }));
|
||||
|
||||
let fork_state = collect_state(&fork_entries);
|
||||
assert_eq!(fork_state.history.len(), original_history_len);
|
||||
@@ -337,9 +337,9 @@ async fn session_fork_at_truncates() {
|
||||
let client = MockLlmClient::new(simple_text_events());
|
||||
let worker = Worker::new(client);
|
||||
|
||||
let sid = session_store::create_session(
|
||||
let sid = session_store::create_segment(
|
||||
&store,
|
||||
SessionStartState {
|
||||
SegmentStartState {
|
||||
system_prompt: worker.get_system_prompt(),
|
||||
config: worker.request_config(),
|
||||
history: worker.history(),
|
||||
@@ -356,7 +356,7 @@ async fn session_fork_at_truncates() {
|
||||
let fork_id = session_store::fork_at(&store, sid, worker.turn_count()).unwrap();
|
||||
|
||||
let fork_entries = store.read_all(fork_id).unwrap();
|
||||
assert_eq!(fork_entries.len(), 1); // Just the new SessionStart
|
||||
assert_eq!(fork_entries.len(), 1); // Just the new SegmentStart
|
||||
|
||||
let fork_state = collect_state(&fork_entries);
|
||||
// History at fork point should match history right after the TurnEnd in
|
||||
@@ -375,9 +375,9 @@ async fn session_config_changed_logged() {
|
||||
let client = MockLlmClient::new(vec![]);
|
||||
let mut worker = Worker::new(client);
|
||||
|
||||
let sid = session_store::create_session(
|
||||
let sid = session_store::create_segment(
|
||||
&store,
|
||||
SessionStartState {
|
||||
SegmentStartState {
|
||||
system_prompt: worker.get_system_prompt(),
|
||||
config: worker.request_config(),
|
||||
history: worker.history(),
|
||||
@@ -408,17 +408,17 @@ async fn session_auto_forks_on_conflict() {
|
||||
let client_a = MockLlmClient::new(simple_text_events());
|
||||
let worker_a = Worker::new(client_a);
|
||||
|
||||
let original_sid = session_store::create_session(
|
||||
let original_sid = session_store::create_segment(
|
||||
&store,
|
||||
SessionStartState {
|
||||
SegmentStartState {
|
||||
system_prompt: worker_a.get_system_prompt(),
|
||||
config: worker_a.request_config(),
|
||||
history: worker_a.history(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let mut session_id = original_sid;
|
||||
// Writer tracked: just the SessionStart we wrote.
|
||||
let mut segment_id = original_sid;
|
||||
// Writer tracked: just the SegmentStart we wrote.
|
||||
let mut entries_written: usize = 1;
|
||||
|
||||
// Simulate another Pod writing to the same session behind our back.
|
||||
@@ -431,9 +431,9 @@ async fn session_auto_forks_on_conflict() {
|
||||
// Now the on-disk count exceeds our tally — ensure_head_or_fork should auto-fork.
|
||||
session_store::ensure_head_or_fork(
|
||||
&store,
|
||||
&mut session_id,
|
||||
&mut segment_id,
|
||||
&mut entries_written,
|
||||
SessionStartState {
|
||||
SegmentStartState {
|
||||
system_prompt: worker_a.get_system_prompt(),
|
||||
config: worker_a.request_config(),
|
||||
history: worker_a.history(),
|
||||
@@ -441,11 +441,11 @@ async fn session_auto_forks_on_conflict() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// session_id should now be different
|
||||
assert_ne!(session_id, original_sid);
|
||||
// segment_id should now be different
|
||||
assert_ne!(segment_id, original_sid);
|
||||
|
||||
// The fork session should exist and have entries
|
||||
let fork_entries = store.read_all(session_id).unwrap();
|
||||
let fork_entries = store.read_all(segment_id).unwrap();
|
||||
assert!(!fork_entries.is_empty());
|
||||
|
||||
// Original session should still have the interloper entry
|
||||
|
||||
Reference in New Issue
Block a user