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:
2026-05-20 05:06:04 +09:00
parent de549812ab
commit 22f5d02385
55 changed files with 611 additions and 610 deletions
+1 -1
View File
@@ -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`.
+15 -15
View File
@@ -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)
}
+15 -15
View File
@@ -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(),
+20 -20
View File
@@ -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>;
}
+1 -1
View File
@@ -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.