Merge: segment-rename

This commit is contained in:
Keisuke Hirata 2026-05-20 05:18:11 +09:00
commit e4cda5d3f2
No known key found for this signature in database
60 changed files with 648 additions and 683 deletions

View File

@ -8,7 +8,6 @@
- Pod: 任意ターンからの Fork複数ターン巻き戻しを汎用化 → [tickets/pod-session-fork.md](tickets/pod-session-fork.md) - Pod: 任意ターンからの Fork複数ターン巻き戻しを汎用化 → [tickets/pod-session-fork.md](tickets/pod-session-fork.md)
- Pod: Inbound PodEvent ハンドリングの重複を統合 → [tickets/pod-inbound-pod-event-dedup.md](tickets/pod-inbound-pod-event-dedup.md) - Pod: Inbound PodEvent ハンドリングの重複を統合 → [tickets/pod-inbound-pod-event-dedup.md](tickets/pod-inbound-pod-event-dedup.md)
- 永続化層整理 (Storage) - 永続化層整理 (Storage)
- SessionId → SegmentId リネーム → [tickets/segment-rename.md](tickets/segment-rename.md)
- Session (Segment 群の grouping) 導入 → [tickets/session-grouping-introduce.md](tickets/session-grouping-introduce.md) - Session (Segment 群の grouping) 導入 → [tickets/session-grouping-introduce.md](tickets/session-grouping-introduce.md)
- live auto-fork の marker 形式確定 → [tickets/live-fork-marker.md](tickets/live-fork-marker.md) - live auto-fork の marker 形式確定 → [tickets/live-fork-marker.md](tickets/live-fork-marker.md)
- Pod 単位永続化 - Pod 単位永続化

View File

@ -51,7 +51,7 @@ pub(crate) struct ResponsesRequest {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>, pub top_p: Option<f32>,
/// 会話単位の安定キー。ChatGPT backend (codex-oauth) は明示キーが /// 会話単位の安定キー。ChatGPT backend (codex-oauth) は明示キーが
/// 無いとプロンプトキャッシュがほぼ効かない。pod 側は `SessionId` /// 無いとプロンプトキャッシュがほぼ効かない。pod 側は `SegmentId`
/// を渡す。`Request::cache_key` が `None` のときはキー自体を送らない。 /// を渡す。`Request::cache_key` が `None` のときはキー自体を送らない。
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub prompt_cache_key: Option<String>, pub prompt_cache_key: Option<String>,

View File

@ -492,7 +492,7 @@ pub struct Request {
/// 会話単位の安定キー。`prompt_cache_key` として送られる /// 会話単位の安定キー。`prompt_cache_key` として送られる
/// (OpenAI Responses)。ChatGPT backend (codex-oauth) は明示キーが /// (OpenAI Responses)。ChatGPT backend (codex-oauth) は明示キーが
/// 無いと org/project ハッシュ衝突でプロンプトキャッシュが /// 無いと org/project ハッシュ衝突でプロンプトキャッシュが
/// ほぼヒットしないため、pod 側で `SessionId` を渡す運用を想定。 /// ほぼヒットしないため、pod 側で `SegmentId` を渡す運用を想定。
/// `cache_anchor` と違い名前空間キーであり、`prefix anchor` とは /// `cache_anchor` と違い名前空間キーであり、`prefix anchor` とは
/// 別の概念。`cache_anchor` を読まない provider と同じく、 /// 別の概念。`cache_anchor` を読まない provider と同じく、
/// `prompt_cache_key` を持たない provider は無視する。 /// `prompt_cache_key` を持たない provider は無視する。

View File

@ -213,7 +213,7 @@ pub struct Worker<C: LlmClient, S: WorkerState = Mutable> {
cache_anchor: Option<usize>, cache_anchor: Option<usize>,
/// Conversation-scoped cache key, set by higher layers. Plumbed into /// Conversation-scoped cache key, set by higher layers. Plumbed into
/// [`Request::cache_key`] at request build time. Pod 側では /// [`Request::cache_key`] at request build time. Pod 側では
/// `SessionId` を渡す。 /// `SegmentId` を渡す。
cache_key: Option<String>, cache_key: Option<String>,
/// State marker /// State marker
_state: PhantomData<S>, _state: PhantomData<S>,

View File

@ -243,7 +243,7 @@ mod tests {
let (_id, _) = write_staging( let (_id, _) = write_staging(
&layout, &layout,
SourceRef { SourceRef {
session_id: "s".into(), segment_id: "s".into(),
range: [0, 1], range: [0, 1],
}, },
ExtractedPayload::default(), ExtractedPayload::default(),

View File

@ -260,7 +260,7 @@ mod tests {
let (id_a, _) = write_staging( let (id_a, _) = write_staging(
&layout, &layout,
SourceRef { SourceRef {
session_id: "s".into(), segment_id: "s".into(),
range: [0, 0], range: [0, 0],
}, },
ExtractedPayload::default(), ExtractedPayload::default(),
@ -269,7 +269,7 @@ mod tests {
let (id_b, _) = write_staging( let (id_b, _) = write_staging(
&layout, &layout,
SourceRef { SourceRef {
session_id: "s".into(), segment_id: "s".into(),
range: [1, 1], range: [1, 1],
}, },
ExtractedPayload::default(), ExtractedPayload::default(),

View File

@ -94,9 +94,9 @@ mod tests {
ExtractedPayload::default() ExtractedPayload::default()
} }
fn source(session_id: &str, range: [u64; 2]) -> SourceRef { fn source(segment_id: &str, range: [u64; 2]) -> SourceRef {
SourceRef { SourceRef {
session_id: session_id.into(), segment_id: segment_id.into(),
range, range,
} }
} }

View File

@ -305,7 +305,7 @@ mod tests {
fn flags_sources_overflow() { fn flags_sources_overflow() {
let (dir, layout) = workspace(); let (dir, layout) = workspace();
let many_sources: String = (0..15) let many_sources: String = (0..15)
.map(|i| format!(" - session_id: s{i}\n range: [{i}, {i}]\n")) .map(|i| format!(" - segment_id: s{i}\n range: [{i}, {i}]\n"))
.collect(); .collect();
write( write(
&dir.path().join(".insomnia/memory/decisions/big.md"), &dir.path().join(".insomnia/memory/decisions/big.md"),

View File

@ -13,7 +13,7 @@
//! session-store の `LogEntry::Extension`、domain `"memory.extract"`)は //! session-store の `LogEntry::Extension`、domain `"memory.extract"`)は
//! Pod 側が責務を持つ。 //! Pod 側が責務を持つ。
//! //!
//! 出力 JSON の wrap は [`write_staging`] が `source: { session_id, range }` //! 出力 JSON の wrap は [`write_staging`] が `source: { segment_id, range }`
//! を機械付与する形で担当し、LLM には source を推論させない。 //! を機械付与する形で担当し、LLM には source を推論させない。
mod input; mod input;

View File

@ -78,7 +78,7 @@ pub struct RequestEntry {
/// staging に書き出される 1 ファイル分のレコード。 /// staging に書き出される 1 ファイル分のレコード。
/// ///
/// `source` は Pod 側ラッパーが session_id と log entry range を /// `source` は Pod 側ラッパーが segment_id と log entry range を
/// 機械付与する。LLM はこのフィールドを見ない / 推論しない。 /// 機械付与する。LLM はこのフィールドを見ない / 推論しない。
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StagingRecord { pub struct StagingRecord {

View File

@ -71,7 +71,7 @@ mod tests {
let layout = WorkspaceLayout::new(tmp.path().to_path_buf()); let layout = WorkspaceLayout::new(tmp.path().to_path_buf());
let source = SourceRef { let source = SourceRef {
session_id: "sess-1".into(), segment_id: "sess-1".into(),
range: [3, 7], range: [3, 7],
}; };
let payload = ExtractedPayload { let payload = ExtractedPayload {
@ -93,7 +93,7 @@ mod tests {
let written: StagingRecord = let written: StagingRecord =
serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap(); serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(written.source.session_id, "sess-1"); assert_eq!(written.source.segment_id, "sess-1");
assert_eq!(written.source.range, [3, 7]); assert_eq!(written.source.range, [3, 7]);
assert_eq!(written.payload.decisions.len(), 1); assert_eq!(written.payload.decisions.len(), 1);
} }
@ -103,7 +103,7 @@ mod tests {
let tmp = tempfile::TempDir::new().unwrap(); let tmp = tempfile::TempDir::new().unwrap();
let layout = WorkspaceLayout::new(tmp.path().to_path_buf()); let layout = WorkspaceLayout::new(tmp.path().to_path_buf());
let source = SourceRef { let source = SourceRef {
session_id: "sess".into(), segment_id: "sess".into(),
range: [0, 0], range: [0, 0],
}; };
let (_, path) = write_staging(&layout, source, ExtractedPayload::default()).unwrap(); let (_, path) = write_staging(&layout, source, ExtractedPayload::default()).unwrap();

View File

@ -10,7 +10,7 @@ pub use lint_common::Frontmatter;
/// `last_sources` arrays for traceability back to raw session logs. /// `last_sources` arrays for traceability back to raw session logs.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceRef { pub struct SourceRef {
pub session_id: String, pub segment_id: String,
/// `[start_entry, end_entry]` inclusive range of session-store entry indices. /// `[start_entry, end_entry]` inclusive range of session-store entry indices.
pub range: [u64; 2], pub range: [u64; 2],
} }

View File

@ -60,7 +60,7 @@ impl Tool for ReadTool {
})?; })?;
let text = String::from_utf8_lossy(&bytes).into_owned(); let text = String::from_utf8_lossy(&bytes).into_owned();
if let Some(session_id) = self.usage_session_id.as_deref() { if let Some(segment_id) = self.usage_session_id.as_deref() {
let usage_slug = params.slug.as_deref().unwrap_or("summary"); let usage_slug = params.slug.as_deref().unwrap_or("summary");
let snapshot = usage::snapshot_record_from_bytes( let snapshot = usage::snapshot_record_from_bytes(
params.kind.record_kind(), params.kind.record_kind(),
@ -69,7 +69,7 @@ impl Tool for ReadTool {
); );
if let Err(err) = usage::append_use_event( if let Err(err) = usage::append_use_event(
&self.layout, &self.layout,
session_id.to_string(), segment_id.to_string(),
UsageSource::MemoryRead, UsageSource::MemoryRead,
vec![snapshot], vec![snapshot],
) { ) {
@ -140,9 +140,9 @@ pub fn read_tool(layout: WorkspaceLayout) -> ToolDefinition {
pub fn read_tool_with_usage( pub fn read_tool_with_usage(
layout: WorkspaceLayout, layout: WorkspaceLayout,
session_id: impl Into<String>, segment_id: impl Into<String>,
) -> ToolDefinition { ) -> ToolDefinition {
read_tool_inner(layout, Some(session_id.into())) read_tool_inner(layout, Some(segment_id.into()))
} }
fn read_tool_inner(layout: WorkspaceLayout, usage_session_id: Option<String>) -> ToolDefinition { fn read_tool_inner(layout: WorkspaceLayout, usage_session_id: Option<String>) -> ToolDefinition {

View File

@ -64,7 +64,7 @@ impl UsageRecordSnapshot {
pub struct UsageEvent { pub struct UsageEvent {
pub id: Uuid, pub id: Uuid,
pub occurred_at: DateTime<Utc>, pub occurred_at: DateTime<Utc>,
pub session_id: String, pub segment_id: String,
pub event: UsageEventKind, pub event: UsageEventKind,
pub source: UsageSource, pub source: UsageSource,
pub records: Vec<UsageRecordSnapshot>, pub records: Vec<UsageRecordSnapshot>,
@ -72,7 +72,7 @@ pub struct UsageEvent {
impl UsageEvent { impl UsageEvent {
pub fn new( pub fn new(
session_id: impl Into<String>, segment_id: impl Into<String>,
event: UsageEventKind, event: UsageEventKind,
source: UsageSource, source: UsageSource,
records: Vec<UsageRecordSnapshot>, records: Vec<UsageRecordSnapshot>,
@ -80,7 +80,7 @@ impl UsageEvent {
Self { Self {
id: Uuid::now_v7(), id: Uuid::now_v7(),
occurred_at: Utc::now(), occurred_at: Utc::now(),
session_id: session_id.into(), segment_id: segment_id.into(),
event, event,
source, source,
records, records,
@ -144,7 +144,7 @@ pub fn append_usage_event(layout: &WorkspaceLayout, event: &UsageEvent) -> io::R
/// Convenience for a successful explicit record read. /// Convenience for a successful explicit record read.
pub fn append_use_event( pub fn append_use_event(
layout: &WorkspaceLayout, layout: &WorkspaceLayout,
session_id: impl Into<String>, segment_id: impl Into<String>,
source: UsageSource, source: UsageSource,
records: Vec<UsageRecordSnapshot>, records: Vec<UsageRecordSnapshot>,
) -> io::Result<()> { ) -> io::Result<()> {
@ -153,14 +153,14 @@ pub fn append_use_event(
} }
append_usage_event( append_usage_event(
layout, layout,
&UsageEvent::new(session_id, UsageEventKind::Use, source, records), &UsageEvent::new(segment_id, UsageEventKind::Use, source, records),
) )
} }
/// Convenience for resident model-invocation exposure cost telemetry. /// Convenience for resident model-invocation exposure cost telemetry.
pub fn append_resident_exposure_event( pub fn append_resident_exposure_event(
layout: &WorkspaceLayout, layout: &WorkspaceLayout,
session_id: impl Into<String>, segment_id: impl Into<String>,
records: Vec<UsageRecordSnapshot>, records: Vec<UsageRecordSnapshot>,
) -> io::Result<()> { ) -> io::Result<()> {
if records.is_empty() { if records.is_empty() {
@ -169,7 +169,7 @@ pub fn append_resident_exposure_event(
append_usage_event( append_usage_event(
layout, layout,
&UsageEvent::new( &UsageEvent::new(
session_id, segment_id,
UsageEventKind::ResidentExposure, UsageEventKind::ResidentExposure,
UsageSource::ResidentInjection, UsageSource::ResidentInjection,
records, records,

View File

@ -4,7 +4,7 @@ use std::io;
use std::path::PathBuf; use std::path::PathBuf;
use manifest::ScopeRule; use manifest::ScopeRule;
use session_store::SessionId; use session_store::SegmentId;
/// Errors raised by the mutating pod-registry operations. /// Errors raised by the mutating pod-registry operations.
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
@ -27,11 +27,11 @@ pub enum ScopeLockError {
#[error("pod `{0}` is not registered")] #[error("pod `{0}` is not registered")]
UnknownPod(String), UnknownPod(String),
#[error( #[error(
"session {session_id} is already held by pod `{pod_name}` at {}", "session {segment_id} is already held by pod `{pod_name}` at {}",
.socket.display() .socket.display()
)] )]
SessionConflict { SegmentConflict {
session_id: SessionId, segment_id: SegmentId,
pod_name: String, pod_name: String,
socket: PathBuf, socket: PathBuf,
}, },

View File

@ -27,8 +27,8 @@ pub use conflict::{
}; };
pub use error::ScopeLockError; pub use error::ScopeLockError;
pub use lifecycle::{ pub use lifecycle::{
ScopeAllocationGuard, SessionLockInfo, adopt_allocation, install_top_level, ScopeAllocationGuard, SegmentLockInfo, adopt_allocation, install_top_level,
install_top_level_with_deny, lookup_session, update_session, install_top_level_with_deny, lookup_segment, update_segment,
}; };
pub use mutate::{ pub use mutate::{
delegate_scope, reclaim_stale, reclaim_stale_with, register_pod, register_pod_with_deny, delegate_scope, reclaim_stale, reclaim_stale_with, register_pod, register_pod_with_deny,

View File

@ -5,7 +5,7 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use manifest::ScopeRule; use manifest::ScopeRule;
use session_store::SessionId; use session_store::SegmentId;
use crate::error::ScopeLockError; use crate::error::ScopeLockError;
use crate::mutate::release_pod; use crate::mutate::release_pod;
@ -45,9 +45,9 @@ pub fn install_top_level(
pid: u32, pid: u32,
socket: PathBuf, socket: PathBuf,
scope_allow: Vec<ScopeRule>, scope_allow: Vec<ScopeRule>,
session_id: SessionId, segment_id: SegmentId,
) -> Result<ScopeAllocationGuard, ScopeLockError> { ) -> Result<ScopeAllocationGuard, ScopeLockError> {
install_top_level_with_deny(pod_name, pid, socket, scope_allow, Vec::new(), session_id) install_top_level_with_deny(pod_name, pid, socket, scope_allow, Vec::new(), segment_id)
} }
/// Open the default lock file, register a top-level Pod with explicit /// Open the default lock file, register a top-level Pod with explicit
@ -59,7 +59,7 @@ pub fn install_top_level_with_deny(
socket: PathBuf, socket: PathBuf,
scope_allow: Vec<ScopeRule>, scope_allow: Vec<ScopeRule>,
scope_deny: Vec<ScopeRule>, scope_deny: Vec<ScopeRule>,
session_id: SessionId, segment_id: SegmentId,
) -> Result<ScopeAllocationGuard, ScopeLockError> { ) -> Result<ScopeAllocationGuard, ScopeLockError> {
let lock_path = default_registry_path()?; let lock_path = default_registry_path()?;
let mut guard = LockFileGuard::open(&lock_path)?; let mut guard = LockFileGuard::open(&lock_path)?;
@ -70,7 +70,7 @@ pub fn install_top_level_with_deny(
socket, socket,
scope_allow, scope_allow,
scope_deny, scope_deny,
session_id, segment_id,
)?; )?;
Ok(ScopeAllocationGuard { Ok(ScopeAllocationGuard {
pod_name, pod_name,
@ -83,14 +83,14 @@ pub fn install_top_level_with_deny(
/// ///
/// The spawning flow is two-stage: the spawner calls /// The spawning flow is two-stage: the spawner calls
/// [`crate::delegate_scope`] (with its own pid as a live placeholder, /// [`crate::delegate_scope`] (with its own pid as a live placeholder,
/// `session_id = None`), then exec's the child; the child, once /// `segment_id = None`), then exec's the child; the child, once
/// running, calls this function to rewrite the allocation's pid + /// running, calls this function to rewrite the allocation's pid +
/// session_id to its own and claim the [`ScopeAllocationGuard`] so /// segment_id to its own and claim the [`ScopeAllocationGuard`] so
/// the entry is released when the child exits. /// the entry is released when the child exits.
pub fn adopt_allocation( pub fn adopt_allocation(
pod_name: String, pod_name: String,
new_pid: u32, new_pid: u32,
session_id: SessionId, segment_id: SegmentId,
) -> Result<ScopeAllocationGuard, ScopeLockError> { ) -> Result<ScopeAllocationGuard, ScopeLockError> {
let lock_path = default_registry_path()?; let lock_path = default_registry_path()?;
let mut guard = LockFileGuard::open(&lock_path)?; let mut guard = LockFileGuard::open(&lock_path)?;
@ -99,7 +99,7 @@ pub fn adopt_allocation(
.find_mut(&pod_name) .find_mut(&pod_name)
.ok_or_else(|| ScopeLockError::UnknownPod(pod_name.clone()))?; .ok_or_else(|| ScopeLockError::UnknownPod(pod_name.clone()))?;
alloc.pid = new_pid; alloc.pid = new_pid;
alloc.session_id = Some(session_id); alloc.segment_id = Some(segment_id);
guard.save()?; guard.save()?;
Ok(ScopeAllocationGuard { Ok(ScopeAllocationGuard {
pod_name, pod_name,
@ -107,32 +107,32 @@ pub fn adopt_allocation(
}) })
} }
/// Rewrite the `session_id` recorded for `pod_name` to /// Rewrite the `segment_id` recorded for `pod_name` to
/// `new_session_id`. /// `new_segment_id`.
/// ///
/// The Pod's in-memory `session_id` can change underneath the /// The Pod's in-memory `segment_id` can change underneath the
/// allocation in two normal places: /// allocation in two normal places:
/// ///
/// - `Pod::compact` mints a fresh session and swaps it in. /// - `Pod::compact` mints a fresh session and swaps it in.
/// - `session_store::ensure_head_or_fork` auto-forks when another /// - `session_store::ensure_head_or_fork` auto-forks when another
/// writer has advanced the store head behind our back. /// writer has advanced the store head behind our back.
/// ///
/// Both paths must call this so subsequent [`lookup_session`] queries /// Both paths must call this so subsequent [`lookup_segment`] queries
/// find the live session id, not the old one. Without this update a /// find the live session id, not the old one. Without this update a
/// concurrent `restore_from_manifest(new_id)` would see "no live /// concurrent `restore_from_manifest(new_id)` would see "no live
/// writer" and proceed to register a competing allocation on the /// writer" and proceed to register a competing allocation on the
/// session this Pod just moved into. /// session this Pod just moved into.
/// ///
/// The lock is opened once and the allocation is rewritten inside the /// The lock is opened once and the allocation is rewritten inside the
/// guard, so the session_id collision check is atomic with the /// guard, so the segment_id collision check is atomic with the
/// rewrite. /// rewrite.
pub fn update_session(pod_name: &str, new_session_id: SessionId) -> Result<(), ScopeLockError> { pub fn update_segment(pod_name: &str, new_segment_id: SegmentId) -> Result<(), ScopeLockError> {
let lock_path = default_registry_path()?; let lock_path = default_registry_path()?;
let mut guard = LockFileGuard::open(&lock_path)?; let mut guard = LockFileGuard::open(&lock_path)?;
if let Some(other) = guard.data().find_by_session(new_session_id) { if let Some(other) = guard.data().find_by_segment(new_segment_id) {
if other.pod_name != pod_name { if other.pod_name != pod_name {
return Err(ScopeLockError::SessionConflict { return Err(ScopeLockError::SegmentConflict {
session_id: new_session_id, segment_id: new_segment_id,
pod_name: other.pod_name.clone(), pod_name: other.pod_name.clone(),
socket: other.socket.clone(), socket: other.socket.clone(),
}); });
@ -142,7 +142,7 @@ pub fn update_session(pod_name: &str, new_session_id: SessionId) -> Result<(), S
.data_mut() .data_mut()
.find_mut(pod_name) .find_mut(pod_name)
.ok_or_else(|| ScopeLockError::UnknownPod(pod_name.into()))?; .ok_or_else(|| ScopeLockError::UnknownPod(pod_name.into()))?;
alloc.session_id = Some(new_session_id); alloc.segment_id = Some(new_segment_id);
guard.save()?; guard.save()?;
Ok(()) Ok(())
} }
@ -150,25 +150,25 @@ pub fn update_session(pod_name: &str, new_session_id: SessionId) -> Result<(), S
/// Information about a Pod that currently holds an allocation for a /// Information about a Pod that currently holds an allocation for a
/// given session. /// given session.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SessionLockInfo { pub struct SegmentLockInfo {
pub pod_name: String, pub pod_name: String,
pub socket: PathBuf, pub socket: PathBuf,
pub pid: u32, pub pid: u32,
} }
/// Open the default lock file, reclaim stale entries, and return the /// Open the default lock file, reclaim stale entries, and return the
/// allocation currently writing to `session_id`, if any. /// allocation currently writing to `segment_id`, if any.
/// ///
/// Used by `Pod::restore_from_manifest` to refuse a resume that would /// Used by `Pod::restore_from_manifest` to refuse a resume that would
/// race a live writer on the same source session. /// race a live writer on the same source session.
pub fn lookup_session(session_id: SessionId) -> Result<Option<SessionLockInfo>, ScopeLockError> { pub fn lookup_segment(segment_id: SegmentId) -> Result<Option<SegmentLockInfo>, ScopeLockError> {
let lock_path = default_registry_path()?; let lock_path = default_registry_path()?;
let mut guard = LockFileGuard::open(&lock_path)?; let mut guard = LockFileGuard::open(&lock_path)?;
crate::mutate::reclaim_stale(&mut guard); crate::mutate::reclaim_stale(&mut guard);
Ok(guard Ok(guard
.data() .data()
.find_by_session(session_id) .find_by_segment(segment_id)
.map(|a| SessionLockInfo { .map(|a| SegmentLockInfo {
pod_name: a.pod_name.clone(), pod_name: a.pod_name.clone(),
socket: a.socket.clone(), socket: a.socket.clone(),
pid: a.pid, pid: a.pid,
@ -193,7 +193,7 @@ mod tests {
scope_allow: vec![write_rule("/tmp/child", true)], scope_allow: vec![write_rule("/tmp/child", true)],
scope_deny: Vec::new(), scope_deny: Vec::new(),
delegated_from: None, delegated_from: None,
session_id: None, segment_id: None,
}); });
g.save().unwrap(); g.save().unwrap();
} }
@ -267,12 +267,12 @@ mod tests {
s, s,
) )
.unwrap(); .unwrap();
let info = lookup_session(s).unwrap().expect("expected live writer"); let info = lookup_segment(s).unwrap().expect("expected live writer");
assert_eq!(info.pod_name, "live"); assert_eq!(info.pod_name, "live");
assert_eq!(info.socket, sock("live")); assert_eq!(info.socket, sock("live"));
drop(guard); drop(guard);
// After the guard's release, the lookup goes back to None. // After the guard's release, the lookup goes back to None.
assert!(lookup_session(s).unwrap().is_none()); assert!(lookup_segment(s).unwrap().is_none());
} }
#[test] #[test]
@ -289,10 +289,10 @@ mod tests {
original, original,
) )
.unwrap(); .unwrap();
update_session("p", updated).unwrap(); update_segment("p", updated).unwrap();
// lookup against the original is now empty, the updated id wins. // lookup against the original is now empty, the updated id wins.
assert!(lookup_session(original).unwrap().is_none()); assert!(lookup_segment(original).unwrap().is_none());
assert_eq!(lookup_session(updated).unwrap().unwrap().pod_name, "p"); assert_eq!(lookup_segment(updated).unwrap().unwrap().pod_name, "p");
} }
#[test] #[test]
@ -318,17 +318,17 @@ mod tests {
) )
.unwrap(); .unwrap();
// `a` cannot adopt b's live session id. // `a` cannot adopt b's live session id.
let err = update_session("a", s_b).unwrap_err(); let err = update_segment("a", s_b).unwrap_err();
match err { match err {
ScopeLockError::SessionConflict { ScopeLockError::SegmentConflict {
pod_name, pod_name,
session_id, segment_id,
.. ..
} => { } => {
assert_eq!(pod_name, "b"); assert_eq!(pod_name, "b");
assert_eq!(session_id, s_b); assert_eq!(segment_id, s_b);
} }
other => panic!("expected SessionConflict, got {other:?}"), other => panic!("expected SegmentConflict, got {other:?}"),
} }
} }
} }

View File

@ -5,7 +5,7 @@ use std::io;
use std::path::PathBuf; use std::path::PathBuf;
use manifest::{Permission, ScopeRule}; use manifest::{Permission, ScopeRule};
use session_store::SessionId; use session_store::SegmentId;
use crate::conflict::{find_conflict_owner, find_conflict_owners, is_within_effective_write}; use crate::conflict::{find_conflict_owner, find_conflict_owners, is_within_effective_write};
use crate::error::ScopeLockError; use crate::error::ScopeLockError;
@ -16,7 +16,7 @@ use crate::table::{Allocation, LockFileGuard};
/// conflicts so a crashed Pod's allocation doesn't block the new one. /// conflicts so a crashed Pod's allocation doesn't block the new one.
/// ///
/// Rejects when another live allocation is already writing to /// Rejects when another live allocation is already writing to
/// `session_id`, so two `restore_from_manifest` calls under different /// `segment_id`, so two `restore_from_manifest` calls under different
/// `pod_name`s cannot both grab the same session log. /// `pod_name`s cannot both grab the same session log.
pub fn register_pod( pub fn register_pod(
guard: &mut LockFileGuard, guard: &mut LockFileGuard,
@ -24,7 +24,7 @@ pub fn register_pod(
pid: u32, pid: u32,
socket: PathBuf, socket: PathBuf,
scope_allow: Vec<ScopeRule>, scope_allow: Vec<ScopeRule>,
session_id: SessionId, segment_id: SegmentId,
) -> Result<(), ScopeLockError> { ) -> Result<(), ScopeLockError> {
register_pod_with_deny( register_pod_with_deny(
guard, guard,
@ -33,7 +33,7 @@ pub fn register_pod(
socket, socket,
scope_allow, scope_allow,
Vec::new(), Vec::new(),
session_id, segment_id,
) )
} }
@ -56,15 +56,15 @@ pub fn register_pod_with_deny(
socket: PathBuf, socket: PathBuf,
scope_allow: Vec<ScopeRule>, scope_allow: Vec<ScopeRule>,
scope_deny: Vec<ScopeRule>, scope_deny: Vec<ScopeRule>,
session_id: SessionId, segment_id: SegmentId,
) -> Result<(), ScopeLockError> { ) -> Result<(), ScopeLockError> {
reclaim_stale(guard); reclaim_stale(guard);
if guard.data().find(&pod_name).is_some() { if guard.data().find(&pod_name).is_some() {
return Err(ScopeLockError::DuplicatePodName(pod_name)); return Err(ScopeLockError::DuplicatePodName(pod_name));
} }
if let Some(existing) = guard.data().find_by_session(session_id) { if let Some(existing) = guard.data().find_by_segment(segment_id) {
return Err(ScopeLockError::SessionConflict { return Err(ScopeLockError::SegmentConflict {
session_id, segment_id,
pod_name: existing.pod_name.clone(), pod_name: existing.pod_name.clone(),
socket: existing.socket.clone(), socket: existing.socket.clone(),
}); });
@ -99,7 +99,7 @@ pub fn register_pod_with_deny(
scope_allow, scope_allow,
scope_deny, scope_deny,
delegated_from: None, delegated_from: None,
session_id: Some(session_id), segment_id: Some(segment_id),
}); });
guard.save()?; guard.save()?;
Ok(()) Ok(())
@ -147,9 +147,9 @@ pub fn delegate_scope(
scope_allow, scope_allow,
scope_deny: Vec::new(), scope_deny: Vec::new(),
delegated_from: Some(spawner.into()), delegated_from: Some(spawner.into()),
// Pre-reservation. The child fills in its own session_id when // Pre-reservation. The child fills in its own segment_id when
// it calls `adopt_allocation` after the worker is built. // it calls `adopt_allocation` after the worker is built.
session_id: None, segment_id: None,
}); });
guard.save()?; guard.save()?;
Ok(()) Ok(())
@ -587,8 +587,8 @@ mod tests {
shared_session, shared_session,
) )
.unwrap(); .unwrap();
// Second registration tries to grab the same session_id under // Second registration tries to grab the same segment_id under
// a different pod_name. Without the SessionConflict check both // a different pod_name. Without the SegmentConflict check both
// would succeed and race on the same jsonl. // would succeed and race on the same jsonl.
let err = register_pod( let err = register_pod(
&mut g, &mut g,
@ -600,15 +600,15 @@ mod tests {
) )
.unwrap_err(); .unwrap_err();
match err { match err {
ScopeLockError::SessionConflict { ScopeLockError::SegmentConflict {
session_id, segment_id,
pod_name, pod_name,
.. ..
} => { } => {
assert_eq!(session_id, shared_session); assert_eq!(segment_id, shared_session);
assert_eq!(pod_name, "first"); assert_eq!(pod_name, "first");
} }
other => panic!("expected SessionConflict, got {other:?}"), other => panic!("expected SegmentConflict, got {other:?}"),
} }
} }
} }

View File

@ -8,7 +8,7 @@ use std::path::{Path, PathBuf};
use fs4::fs_std::FileExt; use fs4::fs_std::FileExt;
use manifest::{ScopeRule, paths}; use manifest::{ScopeRule, paths};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use session_store::SessionId; use session_store::SegmentId;
/// On-disk representation of the allocation table. /// On-disk representation of the allocation table.
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize)]
@ -43,12 +43,12 @@ pub struct Allocation {
/// Name of the Pod that delegated scope to this one, or `None` for /// Name of the Pod that delegated scope to this one, or `None` for
/// a top-level Pod started directly by a human. /// a top-level Pod started directly by a human.
pub delegated_from: Option<String>, pub delegated_from: Option<String>,
/// Session ID this Pod is currently writing to. `None` means this /// Segment ID this Pod is currently writing to. `None` means this
/// is a pre-reservation made by a spawner via [`crate::delegate_scope`] /// is a pre-reservation made by a spawner via [`crate::delegate_scope`]
/// before the child has come up; the child fills it in at /// before the child has come up; the child fills it in at
/// [`crate::adopt_allocation`] time. /// [`crate::adopt_allocation`] time.
#[serde(default)] #[serde(default)]
pub session_id: Option<SessionId>, pub segment_id: Option<SegmentId>,
} }
impl LockFile { impl LockFile {
@ -60,12 +60,12 @@ impl LockFile {
self.allocations.iter_mut().find(|a| a.pod_name == pod_name) self.allocations.iter_mut().find(|a| a.pod_name == pod_name)
} }
/// Find the allocation currently writing to `session_id`. Skips /// Find the allocation currently writing to `segment_id`. Skips
/// pre-reservations whose `session_id` is still `None`. /// pre-reservations whose `segment_id` is still `None`.
pub fn find_by_session(&self, session_id: SessionId) -> Option<&Allocation> { pub fn find_by_segment(&self, segment_id: SegmentId) -> Option<&Allocation> {
self.allocations self.allocations
.iter() .iter()
.find(|a| a.session_id == Some(session_id)) .find(|a| a.segment_id == Some(segment_id))
} }
} }
@ -225,8 +225,8 @@ mod tests {
let dir = TempDir::new().unwrap(); let dir = TempDir::new().unwrap();
let path = dir.path().join("pods.json"); let path = dir.path().join("pods.json");
let mut g = open_empty(&path); let mut g = open_empty(&path);
// Pre-reservation: delegate_scope leaves session_id = None // Pre-reservation: delegate_scope leaves segment_id = None
// until adopt_allocation rewrites it. find_by_session must not // until adopt_allocation rewrites it. find_by_segment must not
// match those placeholders, otherwise a freshly-spawning child // match those placeholders, otherwise a freshly-spawning child
// would shadow itself before it has even chosen a session. // would shadow itself before it has even chosen a session.
register_pod( register_pod(
@ -249,13 +249,13 @@ mod tests {
.unwrap(); .unwrap();
let target_session = sid(); let target_session = sid();
// The placeholder allocation has session_id = None and must // The placeholder allocation has segment_id = None and must
// not be returned for any lookup. // not be returned for any lookup.
assert!(g.data().find_by_session(target_session).is_none()); assert!(g.data().find_by_segment(target_session).is_none());
// After adopt-style rewrite, the same allocation is now found. // After adopt-style rewrite, the same allocation is now found.
g.data_mut().find_mut("child").unwrap().session_id = Some(target_session); g.data_mut().find_mut("child").unwrap().segment_id = Some(target_session);
let found = g.data().find_by_session(target_session).unwrap(); let found = g.data().find_by_segment(target_session).unwrap();
assert_eq!(found.pod_name, "child"); assert_eq!(found.pod_name, "child");
} }
} }

View File

@ -6,12 +6,12 @@ use std::path::{Path, PathBuf};
use std::sync::{LazyLock, Mutex, MutexGuard}; use std::sync::{LazyLock, Mutex, MutexGuard};
use manifest::{Permission, ScopeRule}; use manifest::{Permission, ScopeRule};
use session_store::SessionId; use session_store::SegmentId;
use crate::table::LockFileGuard; use crate::table::LockFileGuard;
pub(crate) fn sid() -> SessionId { pub(crate) fn sid() -> SegmentId {
session_store::new_session_id() session_store::new_segment_id()
} }
/// Serialises tests that mutate runtime-dir env vars. The test /// Serialises tests that mutate runtime-dir env vars. The test

View File

@ -54,7 +54,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut pod = Pod::from_manifest_toml(&toml, store).await?; let mut pod = Pod::from_manifest_toml(&toml, store).await?;
let manifest: &PodManifest = pod.manifest(); let manifest: &PodManifest = pod.manifest();
println!("Pod: {}", manifest.pod.name); println!("Pod: {}", manifest.pod.name);
println!("Session: {}", pod.session_id()); println!("Segment: {}", pod.segment_id());
// 4. Run a prompt // 4. Run a prompt
let result = pod.run_text("What is the capital of France?").await?; let result = pod.run_text("What is the capital of France?").await?;
@ -76,7 +76,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
// 6. Session ID for potential restore // 6. Session ID for potential restore
println!("\nSession ID: {}", pod.session_id()); println!("\nSegment ID: {}", pod.segment_id());
Ok(()) Ok(())
} }

View File

@ -11,7 +11,7 @@ use crate::ipc::notify_buffer::NotifyBuffer;
use crate::ipc::server::SocketServer; use crate::ipc::server::SocketServer;
use crate::pod::{Pod, PodError, PodRunResult, SystemItemCommitter}; use crate::pod::{Pod, PodError, PodRunResult, SystemItemCommitter};
use crate::runtime::dir::RuntimeDir; use crate::runtime::dir::RuntimeDir;
use crate::session_log_sink::SessionLogSink; use crate::segment_log_sink::SegmentLogSink;
use crate::shared_state::PodSharedState; use crate::shared_state::PodSharedState;
use crate::spawn::comm_tools::{ use crate::spawn::comm_tools::{
list_pods_tool, read_pod_output_tool, send_to_pod_tool, stop_pod_tool, list_pods_tool, read_pod_output_tool, send_to_pod_tool, stop_pod_tool,
@ -33,10 +33,10 @@ pub struct PodHandle {
pub shared_state: Arc<PodSharedState>, pub shared_state: Arc<PodSharedState>,
pub runtime_dir: Arc<RuntimeDir>, pub runtime_dir: Arc<RuntimeDir>,
pub alerter: Alerter, pub alerter: Alerter,
/// Session-log mirror + broadcast handle. The IPC server snapshots /// Segment-log mirror + broadcast handle. The IPC server snapshots
/// it on every new connection (Event::Snapshot) and forwards /// it on every new connection (Event::Snapshot) and forwards
/// subsequent commits (Event::Entry) on the receiver. /// subsequent commits (Event::Entry) on the receiver.
pub sink: SessionLogSink, pub sink: SegmentLogSink,
} }
impl PodHandle { impl PodHandle {
@ -214,7 +214,7 @@ impl PodController {
let greeting = build_greeting(&pod); let greeting = build_greeting(&pod);
let shared_state = Arc::new(PodSharedState::new( let shared_state = Arc::new(PodSharedState::new(
pod.manifest().pod.name.clone(), pod.manifest().pod.name.clone(),
pod.session_id(), pod.segment_id(),
manifest_toml.clone(), manifest_toml.clone(),
greeting, greeting,
)); ));
@ -430,7 +430,7 @@ where
let scope_handle = pod.scope().clone(); let scope_handle = pod.scope().clone();
let pwd = pod.pwd().to_path_buf(); let pwd = pod.pwd().to_path_buf();
let task_store = pod.task_store(); let task_store = pod.task_store();
let session_id_for_usage = pod.session_id().to_string(); let session_id_for_usage = pod.segment_id().to_string();
let scope_change_sink = pod.scope_change_sink(); let scope_change_sink = pod.scope_change_sink();
let memory_config = pod.manifest().memory.clone(); let memory_config = pod.manifest().memory.clone();
let spawner_name = pod.manifest().pod.name.clone(); let spawner_name = pod.manifest().pod.name.clone();
@ -992,7 +992,7 @@ mod tests {
let (cancel_tx, cancel_rx) = mpsc::channel::<()>(1); let (cancel_tx, cancel_rx) = mpsc::channel::<()>(1);
let shared_state = Arc::new(PodSharedState::new( let shared_state = Arc::new(PodSharedState::new(
"child-pod".to_string(), "child-pod".to_string(),
session_store::new_session_id(), session_store::new_segment_id(),
String::new(), String::new(),
protocol::Greeting { protocol::Greeting {
pod_name: "child-pod".to_string(), pod_name: "child-pod".to_string(),

View File

@ -105,10 +105,10 @@ async fn handle_connection(stream: tokio::net::UnixStream, handle: PodHandle) {
match entry { match entry {
Ok(entry) => { Ok(entry) => {
let outbound = match entry { let outbound = match entry {
session_store::LogEntry::SessionStart { .. } => { session_store::LogEntry::SegmentStart { .. } => {
let value = serde_json::to_value(&entry) let value = serde_json::to_value(&entry)
.expect("LogEntry is Serialize"); .expect("LogEntry is Serialize");
Some(Event::SessionRotated { entry: value }) Some(Event::SegmentRotated { entry: value })
} }
session_store::LogEntry::SystemItem { item, .. } => { session_store::LogEntry::SystemItem { item, .. } => {
let value = serde_json::to_value(&item) let value = serde_json::to_value(&item)
@ -119,7 +119,7 @@ async fn handle_connection(stream: tokio::net::UnixStream, handle: PodHandle) {
Some(Event::InvokeStart { kind: trigger }) Some(Event::InvokeStart { kind: trigger })
} }
other => { other => {
// `SessionLogSink::is_live_relevant` keeps // `SegmentLogSink::is_live_relevant` keeps
// non-live-relevant variants off the // non-live-relevant variants off the
// broadcast lane; reaching here means the // broadcast lane; reaching here means the
// two are out of sync and we silently // two are out of sync and we silently

View File

@ -5,7 +5,7 @@ pub mod hook;
pub mod ipc; pub mod ipc;
pub mod prompt; pub mod prompt;
pub mod runtime; pub mod runtime;
pub mod session_log_sink; pub mod segment_log_sink;
pub mod shared_state; pub mod shared_state;
pub mod spawn; pub mod spawn;
pub mod workflow; pub mod workflow;
@ -31,5 +31,5 @@ pub use prompt::system::{SystemPromptContext, SystemPromptError, SystemPromptTem
pub use protocol::{ErrorCode, Event, Method, PodStatus, TurnResult}; pub use protocol::{ErrorCode, Event, Method, PodStatus, TurnResult};
pub use provider::{ProviderError, build_client}; pub use provider::{ProviderError, build_client};
pub use runtime::dir::RuntimeDir; pub use runtime::dir::RuntimeDir;
pub use session_log_sink::SessionLogSink; pub use segment_log_sink::SegmentLogSink;
pub use shared_state::PodSharedState; pub use shared_state::PodSharedState;

View File

@ -5,7 +5,7 @@ use std::process::ExitCode;
use clap::Parser; use clap::Parser;
use manifest::{PodManifest, paths}; use manifest::{PodManifest, paths};
use pod::{Pod, PodController, PodFactory, PromptLoader}; use pod::{Pod, PodController, PodFactory, PromptLoader};
use session_store::{FsStore, SessionId}; use session_store::{FsStore, SegmentId};
const USER_MANIFEST_ENV: &str = "INSOMNIA_USER_MANIFEST"; const USER_MANIFEST_ENV: &str = "INSOMNIA_USER_MANIFEST";
@ -53,7 +53,7 @@ struct Cli {
/// Mutually exclusive with `--adopt` (spawned children always start /// Mutually exclusive with `--adopt` (spawned children always start
/// fresh). /// fresh).
#[arg(long, value_name = "UUID", conflicts_with = "adopt")] #[arg(long, value_name = "UUID", conflicts_with = "adopt")]
session: Option<SessionId>, session: Option<SegmentId>,
} }
fn resolve_manifest(cli: &Cli) -> Result<(PodManifest, PromptLoader), String> { fn resolve_manifest(cli: &Cli) -> Result<(PodManifest, PromptLoader), String> {
@ -185,8 +185,8 @@ async fn main() -> ExitCode {
return ExitCode::FAILURE; return ExitCode::FAILURE;
} }
} }
} else if let Some(source_session_id) = cli.session { } else if let Some(source_segment_id) = cli.session {
match Pod::restore_from_manifest(source_session_id, manifest, store, loader).await { match Pod::restore_from_manifest(source_segment_id, manifest, store, loader).await {
Ok(p) => p, Ok(p) => p,
Err(e) => { Err(e) => {
eprintln!("error: failed to restore pod: {e}"); eprintln!("error: failed to restore pod: {e}");

View File

@ -9,11 +9,11 @@ use llm_worker::llm_client::client::LlmClient;
use llm_worker::state::Mutable; use llm_worker::state::Mutable;
use llm_worker::{ToolOutputLimits, UsageRecord, Worker, WorkerError, WorkerResult}; use llm_worker::{ToolOutputLimits, UsageRecord, Worker, WorkerError, WorkerResult};
use session_store::{ use session_store::{
LogEntry, PodScopeSnapshot, SessionId, Store, StoreError, SystemItem, session_log, to_logged, LogEntry, PodScopeSnapshot, SegmentId, Store, StoreError, SystemItem, segment_log, to_logged,
}; };
use tracing::{info, warn}; use tracing::{info, warn};
use crate::session_log_sink::SessionLogSink; use crate::segment_log_sink::SegmentLogSink;
use manifest::{ use manifest::{
Permission, PodManifest, PodManifestConfig, ResolveError, Scope, ScopeConfig, ScopeError, Permission, PodManifest, PodManifestConfig, ResolveError, Scope, ScopeConfig, ScopeError,
@ -44,33 +44,33 @@ use tokio::task::JoinHandle;
/// Lock-free shared session pointer. /// Lock-free shared session pointer.
/// ///
/// Holds the current `(session_id, entries_written)` pair so that the /// Holds the current `(segment_id, entries_written)` pair so that the
/// Pod and every `LogWriterHandle` clone see a consistent view through /// Pod and every `LogWriterHandle` clone see a consistent view through
/// `Arc`-shared lock-free reads. `session_id` is wrapped in `ArcSwap` /// `Arc`-shared lock-free reads. `segment_id` is wrapped in `ArcSwap`
/// so fork (a rare, run-start-only event) can atomically swap it /// so fork (a rare, run-start-only event) can atomically swap it
/// without taking a mutex on the append hot path. `entries_written` is /// without taking a mutex on the append hot path. `entries_written` is
/// an `AtomicUsize` bumped on every successful append; the writer's /// an `AtomicUsize` bumped on every successful append; the writer's
/// tally is compared against the store's on-disk count to detect /// tally is compared against the store's on-disk count to detect
/// concurrent writers in `ensure_session_head`. /// concurrent writers in `ensure_segment_head`.
pub struct SessionState { pub struct SegmentState {
session_id: ArcSwap<SessionId>, segment_id: ArcSwap<SegmentId>,
entries_written: AtomicUsize, entries_written: AtomicUsize,
} }
impl SessionState { impl SegmentState {
pub fn new(session_id: SessionId, entries_written: usize) -> Arc<Self> { pub fn new(segment_id: SegmentId, entries_written: usize) -> Arc<Self> {
Arc::new(Self { Arc::new(Self {
session_id: ArcSwap::from_pointee(session_id), segment_id: ArcSwap::from_pointee(segment_id),
entries_written: AtomicUsize::new(entries_written), entries_written: AtomicUsize::new(entries_written),
}) })
} }
pub fn session_id(&self) -> SessionId { pub fn segment_id(&self) -> SegmentId {
**self.session_id.load() **self.segment_id.load()
} }
pub fn set_session_id(&self, id: SessionId) { pub fn set_segment_id(&self, id: SegmentId) {
self.session_id.store(Arc::new(id)); self.segment_id.store(Arc::new(id));
} }
pub fn entries_written(&self) -> usize { pub fn entries_written(&self) -> usize {
@ -94,8 +94,8 @@ impl SessionState {
#[derive(Clone)] #[derive(Clone)]
pub struct LogWriterHandle<St: Clone> { pub struct LogWriterHandle<St: Clone> {
pub store: St, pub store: St,
pub state: Arc<SessionState>, pub state: Arc<SegmentState>,
pub sink: SessionLogSink, pub sink: SegmentLogSink,
} }
impl<St> LogWriterHandle<St> impl<St> LogWriterHandle<St>
@ -107,8 +107,8 @@ where
/// writes for `< PIPE_BUF` lines, so no user-space serialization is /// writes for `< PIPE_BUF` lines, so no user-space serialization is
/// needed across appenders. /// needed across appenders.
pub fn append_entry(&self, entry: LogEntry) -> Result<(), StoreError> { pub fn append_entry(&self, entry: LogEntry) -> Result<(), StoreError> {
let session_id = self.state.session_id(); let segment_id = self.state.segment_id();
self.store.append(session_id, &entry)?; self.store.append(segment_id, &entry)?;
self.state.increment_entries(); self.state.increment_entries();
self.sink.publish(entry); self.sink.publish(entry);
Ok(()) Ok(())
@ -128,7 +128,7 @@ where
{ {
fn commit_system_item(&self, item: SystemItem) { fn commit_system_item(&self, item: SystemItem) {
let entry = LogEntry::SystemItem { let entry = LogEntry::SystemItem {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
item, item,
}; };
if let Err(err) = self.append_entry(entry) { if let Err(err) = self.append_entry(entry) {
@ -162,9 +162,9 @@ pub struct Pod<C: LlmClient, St: Store> {
worker: Option<Worker<C, Mutable>>, worker: Option<Worker<C, Mutable>>,
store: St, store: St,
/// Shared session pointer. Source of truth for the Pod's current /// Shared session pointer. Source of truth for the Pod's current
/// `session_id` and append tally. `self.session_id()` is a thin /// `segment_id` and append tally. `self.segment_id()` is a thin
/// wrapper over `session_state.session_id()`. /// wrapper over `segment_state.segment_id()`.
session_state: Arc<SessionState>, segment_state: Arc<SegmentState>,
/// Absolute working directory of the Pod. /// Absolute working directory of the Pod.
pwd: PathBuf, pwd: PathBuf,
/// Shared, atomically-swappable view of the Pod's resolved scope. /// Shared, atomically-swappable view of the Pod's resolved scope.
@ -193,12 +193,12 @@ pub struct Pod<C: LlmClient, St: Store> {
/// Worker (e.g. the savings estimator used by the prune projection) /// Worker (e.g. the savings estimator used by the prune projection)
/// can share the same view via [`Pod::usage_history_handle`]. /// can share the same view via [`Pod::usage_history_handle`].
usage_history: Arc<Mutex<Vec<UsageRecord>>>, usage_history: Arc<Mutex<Vec<UsageRecord>>>,
/// Session-lifetime file-operation tracker from the builtin `tools` /// Pod-lifetime file-operation tracker from the builtin `tools`
/// crate. Populated by the Controller when it registers the builtin /// crate. Populated by the Controller when it registers the builtin
/// tools so that Pod-owned operations (e.g. compaction) can consult /// tools so that Pod-owned operations (e.g. compaction) can consult
/// the recency of touched files. /// the recency of touched files.
tracker: Option<tools::Tracker>, tracker: Option<tools::Tracker>,
/// Session-lifetime task store from the builtin `tools` crate. Shared by /// Pod-lifetime task store from the builtin `tools` crate. Shared by
/// TaskCreate / TaskUpdate / TaskList / TaskGet and preserved across /// TaskCreate / TaskUpdate / TaskList / TaskGet and preserved across
/// compaction by keeping the same handle while the Worker history is /// compaction by keeping the same handle while the Worker history is
/// replaced. Restored Pods reconstruct it by replaying Task* tool calls. /// replaced. Restored Pods reconstruct it by replaying Task* tool calls.
@ -284,7 +284,7 @@ pub struct Pod<C: LlmClient, St: Store> {
memory_task: Option<JoinHandle<()>>, memory_task: Option<JoinHandle<()>>,
/// Typed user submissions in submit order. K-th entry corresponds to /// Typed user submissions in submit order. K-th entry corresponds to
/// the K-th `Item::user_message` in `worker.history()` (modulo seed /// the K-th `Item::user_message` in `worker.history()` (modulo seed
/// history loaded via `SessionStart.history`, whose original segments /// history loaded via `SegmentStart.history`, whose original segments
/// are not preserved). Populated from log on `restore_from_manifest`, /// are not preserved). Populated from log on `restore_from_manifest`,
/// appended after `save_user_input` on each `run`. Pre-`Event::Snapshot` /// appended after `save_user_input` on each `run`. Pre-`Event::Snapshot`
/// this fed `PodSharedState.user_segments`; the new wire format /// this fed `PodSharedState.user_segments`; the new wire format
@ -295,7 +295,7 @@ pub struct Pod<C: LlmClient, St: Store> {
/// every successful `session_store::append_entry` write so connected /// every successful `session_store::append_entry` write so connected
/// clients see a `(snapshot, live)` stream consistent with what's /// clients see a `(snapshot, live)` stream consistent with what's
/// on disk. /// on disk.
sink: SessionLogSink, sink: SegmentLogSink,
/// `true` once `wire_history_persistence` has installed the /// `true` once `wire_history_persistence` has installed the
/// `Worker::on_history_append` callback that commits each appended /// `Worker::on_history_append` callback that commits each appended
/// item as a singular `LogEntry::AssistantItem` / `ToolResult` /// item as a singular `LogEntry::AssistantItem` / `ToolResult`
@ -338,7 +338,7 @@ impl<C: LlmClient + Clone + 'static, St: Store + Clone + 'static> Pod<C, St> {
manifest: self.manifest.clone(), manifest: self.manifest.clone(),
worker: Some(worker), worker: Some(worker),
store: self.store.clone(), store: self.store.clone(),
session_state: self.session_state.clone(), segment_state: self.segment_state.clone(),
pwd: self.pwd.clone(), pwd: self.pwd.clone(),
scope: self.scope.clone(), scope: self.scope.clone(),
hook_builder: HookRegistryBuilder::new(), hook_builder: HookRegistryBuilder::new(),
@ -369,7 +369,7 @@ impl<C: LlmClient + Clone + 'static, St: Store + Clone + 'static> Pod<C, St> {
// The memory-task clone never appends to the session log // The memory-task clone never appends to the session log
// (it only reads `worker.history()`), so a fresh sink is // (it only reads `worker.history()`), so a fresh sink is
// fine — nothing observes its broadcast. // fine — nothing observes its broadcast.
sink: SessionLogSink::new(), sink: SegmentLogSink::new(),
history_persistence_wired: false, history_persistence_wired: false,
log_writer: None, log_writer: None,
} }
@ -382,7 +382,7 @@ impl<C: LlmClient + Clone + 'static, St: Store + Clone + 'static> Pod<C, St> {
pub fn log_writer_handle(&self) -> LogWriterHandle<St> { pub fn log_writer_handle(&self) -> LogWriterHandle<St> {
LogWriterHandle { LogWriterHandle {
store: self.store.clone(), store: self.store.clone(),
state: self.session_state.clone(), state: self.segment_state.clone(),
sink: self.sink.clone(), sink: self.sink.clone(),
} }
} }
@ -422,7 +422,7 @@ impl<C: LlmClient + Clone + 'static, St: Store + Clone + 'static> Pod<C, St> {
) { ) {
return; return;
} }
let entry = session_store::classify_history_item(item, session_log::now_millis()); let entry = session_store::classify_history_item(item, segment_log::now_millis());
if let Err(err) = writer.append_entry(entry) { if let Err(err) = writer.append_entry(entry) {
warn!(error = %err, "history append commit failed; dropping"); warn!(error = %err, "history append commit failed; dropping");
} }
@ -469,16 +469,16 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
pwd: PathBuf, pwd: PathBuf,
scope: Scope, scope: Scope,
) -> Result<Self, PodError> { ) -> Result<Self, PodError> {
// Session creation is deferred to `ensure_session_head` at first // Segment creation is deferred to `ensure_segment_head` at first
// run so a later-installed system-prompt template (see // run so a later-installed system-prompt template (see
// `set_system_prompt_template`) can be captured by `SessionStart`. // `set_system_prompt_template`) can be captured by `SegmentStart`.
let session_id = session_store::new_session_id(); let segment_id = session_store::new_segment_id();
let prompts = PromptCatalog::builtins_only()?; let prompts = PromptCatalog::builtins_only()?;
let mut pod = Self { let mut pod = Self {
manifest, manifest,
worker: Some(worker), worker: Some(worker),
store, store,
session_state: SessionState::new(session_id, 0), segment_state: SegmentState::new(segment_id, 0),
pwd, pwd,
scope: SharedScope::new(scope), scope: SharedScope::new(scope),
hook_builder: HookRegistryBuilder::new(), hook_builder: HookRegistryBuilder::new(),
@ -506,7 +506,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
extract_pointer: Arc::new(Mutex::new(None)), extract_pointer: Arc::new(Mutex::new(None)),
memory_task: None, memory_task: None,
user_segments: Vec::new(), user_segments: Vec::new(),
sink: SessionLogSink::new(), sink: SegmentLogSink::new(),
history_persistence_wired: false, history_persistence_wired: false,
log_writer: None, log_writer: None,
}; };
@ -544,8 +544,8 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
/// The session ID used for persistence. Read lock-free from the /// The session ID used for persistence. Read lock-free from the
/// shared session pointer so fork-time swaps are observed immediately. /// shared session pointer so fork-time swaps are observed immediately.
pub fn session_id(&self) -> SessionId { pub fn segment_id(&self) -> SegmentId {
self.session_state.session_id() self.segment_state.segment_id()
} }
/// The Pod's manifest. /// The Pod's manifest.
@ -604,7 +604,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
/// can restore the narrowed scope instead of reclaiming delegated /// can restore the narrowed scope instead of reclaiming delegated
/// writes. /// writes.
pub fn persist_scope_snapshot(&mut self) -> Result<(), StoreError> { pub fn persist_scope_snapshot(&mut self) -> Result<(), StoreError> {
if self.session_state.entries_written() == 0 { if self.segment_state.entries_written() == 0 {
return Ok(()); return Ok(());
} }
let snapshot = { let snapshot = {
@ -616,7 +616,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
}; };
let payload = serde_json::to_value(&snapshot).expect("PodScopeSnapshot is Serialize"); let payload = serde_json::to_value(&snapshot).expect("PodScopeSnapshot is Serialize");
self.commit_entry(LogEntry::Extension { self.commit_entry(LogEntry::Extension {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
domain: session_store::POD_SCOPE_EXTENSION_DOMAIN.into(), domain: session_store::POD_SCOPE_EXTENSION_DOMAIN.into(),
payload, payload,
}) })
@ -627,9 +627,9 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
/// concurrent appenders — the kernel orders `O_APPEND` writes for /// concurrent appenders — the kernel orders `O_APPEND` writes for
/// lines smaller than `PIPE_BUF`. /// lines smaller than `PIPE_BUF`.
pub(crate) fn commit_entry(&self, entry: LogEntry) -> Result<(), StoreError> { pub(crate) fn commit_entry(&self, entry: LogEntry) -> Result<(), StoreError> {
let session_id = self.session_state.session_id(); let segment_id = self.segment_state.segment_id();
self.store.append(session_id, &entry)?; self.store.append(segment_id, &entry)?;
self.session_state.increment_entries(); self.segment_state.increment_entries();
self.sink.publish(entry); self.sink.publish(entry);
Ok(()) Ok(())
} }
@ -637,7 +637,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
/// Cloneable sink handle. Exposed to the controller so the IPC /// Cloneable sink handle. Exposed to the controller so the IPC
/// layer can `subscribe_with_snapshot` and stream entries to /// layer can `subscribe_with_snapshot` and stream entries to
/// clients without consulting any other state. /// clients without consulting any other state.
pub fn sink(&self) -> SessionLogSink { pub fn sink(&self) -> SegmentLogSink {
self.sink.clone() self.sink.clone()
} }
@ -661,7 +661,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
if let Some(snapshot) = snapshot { if let Some(snapshot) = snapshot {
let payload = serde_json::to_value(&snapshot).expect("PodScopeSnapshot is Serialize"); let payload = serde_json::to_value(&snapshot).expect("PodScopeSnapshot is Serialize");
self.commit_entry(LogEntry::Extension { self.commit_entry(LogEntry::Extension {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
domain: session_store::POD_SCOPE_EXTENSION_DOMAIN.into(), domain: session_store::POD_SCOPE_EXTENSION_DOMAIN.into(),
payload, payload,
})?; })?;
@ -716,7 +716,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
/// Snapshot of the typed user segments tracked alongside worker /// Snapshot of the typed user segments tracked alongside worker
/// history. The K-th entry corresponds to the K-th `Item::user_message` /// history. The K-th entry corresponds to the K-th `Item::user_message`
/// derived from `LogEntry::UserInput` entries (post-compaction); seed /// derived from `LogEntry::UserInput` entries (post-compaction); seed
/// history loaded via `SessionStart.history` does not contribute, /// history loaded via `SegmentStart.history` does not contribute,
/// which is acceptable because the original segments are unrecoverable. /// which is acceptable because the original segments are unrecoverable.
pub fn user_segments(&self) -> &[Vec<Segment>] { pub fn user_segments(&self) -> &[Vec<Segment>] {
&self.user_segments &self.user_segments
@ -829,7 +829,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
fn try_record_metric(&mut self, metric: &session_metrics::Metric) { fn try_record_metric(&mut self, metric: &session_metrics::Metric) {
let payload = serde_json::to_value(metric).expect("Metric is Serialize"); let payload = serde_json::to_value(metric).expect("Metric is Serialize");
let entry = LogEntry::Extension { let entry = LogEntry::Extension {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
domain: session_metrics::DOMAIN.into(), domain: session_metrics::DOMAIN.into(),
payload, payload,
}; };
@ -1143,7 +1143,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
self.ensure_interceptor_installed(); self.ensure_interceptor_installed();
self.ensure_system_prompt_materialized()?; self.ensure_system_prompt_materialized()?;
self.cleanup_finished_memory_task(); self.cleanup_finished_memory_task();
self.ensure_session_head()?; self.ensure_segment_head()?;
if self.should_pre_run_compact() { if self.should_pre_run_compact() {
self.join_memory_task().await; self.join_memory_task().await;
} }
@ -1188,7 +1188,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
// IDLE → active marker. Commits first so the next UserInput entry // IDLE → active marker. Commits first so the next UserInput entry
// is contained inside this Invoke range. See `tickets/invoke-turn-llmcall-semantics.md`. // is contained inside this Invoke range. See `tickets/invoke-turn-llmcall-semantics.md`.
self.commit_entry(LogEntry::Invoke { self.commit_entry(LogEntry::Invoke {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
trigger: protocol::InvokeKind::UserSend, trigger: protocol::InvokeKind::UserSend,
})?; })?;
@ -1196,7 +1196,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
// pushes its flattened copy into history. save_delta deliberately // pushes its flattened copy into history. save_delta deliberately
// skips the resulting `is_user_message()` item to avoid double-write. // skips the resulting `is_user_message()` item to avoid double-write.
self.commit_entry(LogEntry::UserInput { self.commit_entry(LogEntry::UserInput {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
segments: input.clone(), segments: input.clone(),
}) })
?; ?;
@ -1376,7 +1376,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
return; return;
}; };
if let Err(err) = if let Err(err) =
memory::append_use_event(layout, self.session_id().to_string(), source, records) memory::append_use_event(layout, self.segment_id().to_string(), source, records)
{ {
warn!(error = %err, "failed to append memory usage event"); warn!(error = %err, "failed to append memory usage event");
} }
@ -1387,7 +1387,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
return; return;
}; };
if let Err(err) = if let Err(err) =
memory::append_resident_exposure_event(layout, self.session_id().to_string(), records) memory::append_resident_exposure_event(layout, self.segment_id().to_string(), records)
{ {
warn!(error = %err, "failed to append resident exposure event"); warn!(error = %err, "failed to append resident exposure event");
} }
@ -1578,7 +1578,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
// drain. The trailing SystemItem entries (drained by the // drain. The trailing SystemItem entries (drained by the
// PodInterceptor) carry the actual payload. // PodInterceptor) carry the actual payload.
self.commit_entry(LogEntry::Invoke { self.commit_entry(LogEntry::Invoke {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
trigger: kind, trigger: kind,
})?; })?;
@ -1612,17 +1612,17 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
/// ///
/// On the first call for a Pod built via `from_manifest`, the session /// On the first call for a Pod built via `from_manifest`, the session
/// has not been written to the store yet — this is when we append the /// has not been written to the store yet — this is when we append the
/// initial `SessionStart` entry, carrying the system prompt that /// initial `SegmentStart` entry, carrying the system prompt that
/// `ensure_system_prompt_materialized` has just rendered. Subsequent /// `ensure_system_prompt_materialized` has just rendered. Subsequent
/// calls fall through to entry-count comparison, which auto-forks /// calls fall through to entry-count comparison, which auto-forks
/// when another writer has appended behind our back. /// when another writer has appended behind our back.
fn ensure_session_head(&mut self) -> Result<(), PodError> { fn ensure_segment_head(&mut self) -> Result<(), PodError> {
let w = self.worker.as_ref().unwrap(); let w = self.worker.as_ref().unwrap();
let prev_session_id = self.session_state.session_id(); let prev_segment_id = self.segment_state.segment_id();
let entries_written = self.session_state.entries_written(); let entries_written = self.segment_state.entries_written();
if entries_written == 0 { if entries_written == 0 {
let initial = LogEntry::SessionStart { let initial = LogEntry::SegmentStart {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
system_prompt: w.get_system_prompt().map(String::from), system_prompt: w.get_system_prompt().map(String::from),
config: w.request_config().clone(), config: w.request_config().clone(),
history: to_logged(w.history()), history: to_logged(w.history()),
@ -1636,17 +1636,17 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
// Check store count + auto-fork if it drifted. // Check store count + auto-fork if it drifted.
let store_count = self let store_count = self
.store .store
.read_entry_count(prev_session_id) .read_entry_count(prev_segment_id)
.map_err(PodError::from)?; .map_err(PodError::from)?;
if store_count == entries_written { if store_count == entries_written {
return Ok(()); return Ok(());
} }
// Fork: mint a fresh session and switch to it. The new // Fork: mint a fresh session and switch to it. The new
// SessionStart entry replaces the mirror and is broadcast // SegmentStart entry replaces the mirror and is broadcast
// through the sink so existing subscribers reset their view. // through the sink so existing subscribers reset their view.
let fork_id = session_store::new_session_id(); let fork_id = session_store::new_segment_id();
let entry = LogEntry::SessionStart { let entry = LogEntry::SegmentStart {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
system_prompt: w.get_system_prompt().map(String::from), system_prompt: w.get_system_prompt().map(String::from),
config: w.request_config().clone(), config: w.request_config().clone(),
history: to_logged(w.history()), history: to_logged(w.history()),
@ -1654,13 +1654,13 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
compacted_from: None, compacted_from: None,
}; };
self.store self.store
.create_session(fork_id, &[entry.clone()]) .create_segment(fork_id, &[entry.clone()])
.map_err(PodError::from)?; .map_err(PodError::from)?;
self.session_state.set_session_id(fork_id); self.segment_state.set_segment_id(fork_id);
self.session_state.set_entries_written(1); self.segment_state.set_entries_written(1);
self.sink.reset_with_initial(entry); self.sink.reset_with_initial(entry);
if self.scope_allocation.is_some() { if self.scope_allocation.is_some() {
pod_registry::update_session(&self.manifest.pod.name, fork_id)?; pod_registry::update_segment(&self.manifest.pod.name, fork_id)?;
} }
Ok(()) Ok(())
} }
@ -1717,12 +1717,12 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
self.send_event(Event::CompactStart); self.send_event(Event::CompactStart);
match self.compact(retained).await { match self.compact(retained).await {
Ok(new_session_id) => { Ok(new_segment_id) => {
info!( info!(
new_session_id = %new_session_id, new_segment_id = %new_segment_id,
"Compaction succeeded, resuming execution" "Compaction succeeded, resuming execution"
); );
self.send_event(Event::CompactDone { new_session_id }); self.send_event(Event::CompactDone { new_segment_id });
if let Some(ref state) = self.compact_state { if let Some(ref state) = self.compact_state {
state.record_compact_success(); state.record_compact_success();
} }
@ -1767,12 +1767,12 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
let retained = state.retained_tokens(); let retained = state.retained_tokens();
self.send_event(Event::CompactStart); self.send_event(Event::CompactStart);
match self.compact(retained).await { match self.compact(retained).await {
Ok(new_session_id) => { Ok(new_segment_id) => {
info!( info!(
new_session_id = %new_session_id, new_segment_id = %new_segment_id,
"Proactive pre-run compaction succeeded" "Proactive pre-run compaction succeeded"
); );
self.send_event(Event::CompactDone { new_session_id }); self.send_event(Event::CompactDone { new_segment_id });
state.record_compact_success(); state.record_compact_success();
} }
Err(e) => { Err(e) => {
@ -1814,7 +1814,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
.iter() .iter()
.cloned() .cloned()
.collect(); .collect();
let ts = session_log::now_millis(); let ts = segment_log::now_millis();
for item in &new_items { for item in &new_items {
if item.is_user_message() { if item.is_user_message() {
continue; continue;
@ -1837,7 +1837,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
let turn_count = self.worker.as_ref().unwrap().turn_count(); let turn_count = self.worker.as_ref().unwrap().turn_count();
self.commit_entry(LogEntry::TurnEnd { self.commit_entry(LogEntry::TurnEnd {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
turn_count, turn_count,
}) })
?; ?;
@ -1874,7 +1874,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
correlation_id, correlation_id,
} = recorded; } = recorded;
self.commit_entry(LogEntry::LlmUsage { self.commit_entry(LogEntry::LlmUsage {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
history_len: record.history_len, history_len: record.history_len,
input_total_tokens: record.input_total_tokens, input_total_tokens: record.input_total_tokens,
cache_read_tokens: record.cache_read_tokens, cache_read_tokens: record.cache_read_tokens,
@ -1900,7 +1900,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
match result { match result {
Ok(r) => { Ok(r) => {
self.commit_entry(LogEntry::RunCompleted { self.commit_entry(LogEntry::RunCompleted {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
interrupted, interrupted,
result: r.clone(), result: r.clone(),
}) })
@ -1908,7 +1908,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
} }
Err(e) => { Err(e) => {
self.commit_entry(LogEntry::RunErrored { self.commit_entry(LogEntry::RunErrored {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
interrupted, interrupted,
message: e.to_string(), message: e.to_string(),
}) })
@ -1928,7 +1928,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
/// - a clone of the main LlmClient via `clone_boxed()`. /// - a clone of the main LlmClient via `clone_boxed()`.
/// ///
/// Returns the new session ID. /// Returns the new session ID.
pub async fn compact(&mut self, retained_tokens: u64) -> Result<SessionId, PodError> { pub async fn compact(&mut self, retained_tokens: u64) -> Result<SegmentId, PodError> {
use crate::compact::worker::{ use crate::compact::worker::{
CompactWorkerContext, CompactWorkerInterceptor, add_reference_tool, CompactWorkerContext, CompactWorkerInterceptor, add_reference_tool,
mark_read_required_tool, write_summary_tool, mark_read_required_tool, write_summary_tool,
@ -2001,7 +2001,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
.compact_system() .compact_system()
.map_err(PodError::PromptCatalog)?; .map_err(PodError::PromptCatalog)?;
let mut summary_worker = Worker::new(summary_client).system_prompt(summary_system_prompt); let mut summary_worker = Worker::new(summary_client).system_prompt(summary_system_prompt);
summary_worker.set_cache_key(Some(self.session_id().to_string())); summary_worker.set_cache_key(Some(self.segment_id().to_string()));
// Occupancy-based input-token meter + interceptor. The tracker pairs // Occupancy-based input-token meter + interceptor. The tracker pairs
// each pre-request history length with the following UsageEvent, then // each pre-request history length with the following UsageEvent, then
@ -2140,41 +2140,41 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
task_snapshot_text.clone(), task_snapshot_text.clone(),
)); ));
// Build the SessionStart entry for the new compacted session, // Build the SegmentStart entry for the new compacted session,
// then atomically rotate to it: create on disk, swap head, reset // then atomically rotate to it: create on disk, swap head, reset
// the broadcast sink so existing subscribers see the new // the broadcast sink so existing subscribers see the new
// `SessionStart { compacted_from }` and reset their view. // `SegmentStart { compacted_from }` and reset their view.
let new_session_id = session_store::new_session_id(); let new_segment_id = session_store::new_segment_id();
let old_session_id = self.session_state.session_id(); let old_session_id = self.segment_state.segment_id();
let source_turn_count = self.worker.as_ref().unwrap().turn_count(); let source_turn_count = self.worker.as_ref().unwrap().turn_count();
let w = self.worker.as_ref().unwrap(); let w = self.worker.as_ref().unwrap();
let entry = LogEntry::SessionStart { let entry = LogEntry::SegmentStart {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
system_prompt: w.get_system_prompt().map(String::from), system_prompt: w.get_system_prompt().map(String::from),
config: w.request_config().clone(), config: w.request_config().clone(),
history: to_logged(&new_history), history: to_logged(&new_history),
forked_from: None, forked_from: None,
compacted_from: Some(session_store::SessionOrigin { compacted_from: Some(session_store::SegmentOrigin {
session_id: old_session_id, segment_id: old_session_id,
at_turn_index: source_turn_count, at_turn_index: source_turn_count,
}), }),
}; };
self.store.create_session(new_session_id, &[entry.clone()])?; self.store.create_segment(new_segment_id, &[entry.clone()])?;
self.session_state.set_session_id(new_session_id); self.segment_state.set_segment_id(new_segment_id);
self.session_state.set_entries_written(1); self.segment_state.set_entries_written(1);
let session_start = entry; let session_start = entry;
// Broadcast the SessionStart through the sink. This atomically // Broadcast the SegmentStart through the sink. This atomically
// resets the mirror to `[SessionStart]` so any subscriber // resets the mirror to `[SegmentStart]` so any subscriber
// querying after this point sees the post-compaction prefix. // querying after this point sees the post-compaction prefix.
self.sink.reset_with_initial(session_start); self.sink.reset_with_initial(session_start);
// Keep pods.json pointing at the live session_id. Without this // Keep pods.json pointing at the live segment_id. Without this
// a concurrent `restore_from_manifest(new_session_id)` would // a concurrent `restore_from_manifest(new_segment_id)` would
// see no live writer and grab the session this Pod just moved // see no live writer and grab the session this Pod just moved
// into, causing two writers to race on the same jsonl. Skipped // into, causing two writers to race on the same jsonl. Skipped
// when no allocation is installed (e.g. compact under // when no allocation is installed (e.g. compact under
// `Pod::new` in tests). // `Pod::new` in tests).
if self.scope_allocation.is_some() { if self.scope_allocation.is_some() {
pod_registry::update_session(&self.manifest.pod.name, new_session_id)?; pod_registry::update_segment(&self.manifest.pod.name, new_segment_id)?;
} }
// Align user_segments with the post-compaction history. Items // Align user_segments with the post-compaction history. Items
// before `retain_from` (now folded into the summary) lose their // before `retain_from` (now folded into the summary) lose their
@ -2188,8 +2188,8 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
self.worker.as_mut().unwrap().set_history(new_history); self.worker.as_mut().unwrap().set_history(new_history);
// Compaction-introduced system messages are part of the new // Compaction-introduced system messages are part of the new
// SessionStart's history (broadcast above) — clients derive // SegmentStart's history (broadcast above) — clients derive
// their blocks from `SessionStart.history`. No per-item // their blocks from `SegmentStart.history`. No per-item
// broadcast is required. // broadcast is required.
let _ = &compact_introduced_system_messages; let _ = &compact_introduced_system_messages;
let worker = self.worker.as_mut().unwrap(); let worker = self.worker.as_mut().unwrap();
@ -2198,9 +2198,9 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
// compact layout guarantees history[0] is the summary. // compact layout guarantees history[0] is the summary.
worker.set_cache_anchor(Some(0)); worker.set_cache_anchor(Some(0));
// Re-key the OpenAI Responses prompt cache namespace to the new // Re-key the OpenAI Responses prompt cache namespace to the new
// session_id so post-compact turns share a key with extract / // segment_id so post-compact turns share a key with extract /
// consolidate workers running in the same session. // consolidate workers running in the same session.
worker.set_cache_key(Some(new_session_id.to_string())); worker.set_cache_key(Some(new_segment_id.to_string()));
self.usage_history self.usage_history
.lock() .lock()
.expect("usage_history poisoned") .expect("usage_history poisoned")
@ -2219,7 +2219,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
.lock() .lock()
.expect("extract_pointer poisoned") = None; .expect("extract_pointer poisoned") = None;
Ok(new_session_id) Ok(new_segment_id)
} }
/// Build the LlmClient for the compactor Worker. /// Build the LlmClient for the compactor Worker.
@ -2367,7 +2367,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
// Read the session log to get the current entry count. This is // Read the session log to get the current entry count. This is
// the boundary for the source.range end_entry. Called once per // the boundary for the source.range end_entry. Called once per
// extract, on a small local file. // extract, on a small local file.
let entries_now = self.store.read_all(self.session_id())?.len(); let entries_now = self.store.read_all(self.segment_id())?.len();
if entries_now == 0 { if entries_now == 0 {
return Ok(ExtractDecision::Skipped); return Ok(ExtractDecision::Skipped);
} }
@ -2399,7 +2399,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
.memory_extract_system(memory_language) .memory_extract_system(memory_language)
.map_err(PodError::PromptCatalog)?; .map_err(PodError::PromptCatalog)?;
let mut extract_worker = Worker::new(client).system_prompt(extract_system_prompt); let mut extract_worker = Worker::new(client).system_prompt(extract_system_prompt);
extract_worker.set_cache_key(Some(self.session_id().to_string())); extract_worker.set_cache_key(Some(self.segment_id().to_string()));
// Occupancy-based input-token meter + interceptor. The tracker pairs // Occupancy-based input-token meter + interceptor. The tracker pairs
// each pre-request history length with the following UsageEvent, then // each pre-request history length with the following UsageEvent, then
@ -2435,12 +2435,12 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
extract::ExtractedPayload::default() extract::ExtractedPayload::default()
}); });
let source_session_id = self.session_state.session_id(); let source_segment_id = self.segment_state.segment_id();
let staging_id = if payload.is_empty() { let staging_id = if payload.is_empty() {
String::new() String::new()
} else { } else {
let source = memory::schema::SourceRef { let source = memory::schema::SourceRef {
session_id: source_session_id.to_string(), segment_id: source_segment_id.to_string(),
range: [start_entry as u64, end_entry as u64], range: [start_entry as u64, end_entry as u64],
}; };
let (id, _) = extract::write_staging(&layout, source, payload) let (id, _) = extract::write_staging(&layout, source, payload)
@ -2456,7 +2456,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
let payload_value = serde_json::to_value(&pointer_payload) let payload_value = serde_json::to_value(&pointer_payload)
.expect("ExtractPointerPayload is always JSON-serializable"); .expect("ExtractPointerPayload is always JSON-serializable");
self.commit_entry(LogEntry::Extension { self.commit_entry(LogEntry::Extension {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
domain: extract::EXTRACT_DOMAIN.into(), domain: extract::EXTRACT_DOMAIN.into(),
payload: payload_value, payload: payload_value,
})?; })?;
@ -2598,7 +2598,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
} }
}; };
let mut worker = Worker::new(client).system_prompt(consolidation_system_prompt); let mut worker = Worker::new(client).system_prompt(consolidation_system_prompt);
worker.set_cache_key(Some(self.session_id().to_string())); worker.set_cache_key(Some(self.segment_id().to_string()));
// Memory tools are self-contained — they bypass ScopedFs and write // Memory tools are self-contained — they bypass ScopedFs and write
// directly under the workspace via WorkspaceLayout. Resident // directly under the workspace via WorkspaceLayout. Resident
@ -2610,7 +2610,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
let query_cfg = memory::tool::QueryConfig::from(memory_cfg); let query_cfg = memory::tool::QueryConfig::from(memory_cfg);
worker.register_tool(memory::tool::read_tool_with_usage( worker.register_tool(memory::tool::read_tool_with_usage(
layout.clone(), layout.clone(),
self.session_id().to_string(), self.segment_id().to_string(),
)); ));
worker.register_tool(memory::tool::write_tool(layout.clone())); worker.register_tool(memory::tool::write_tool(layout.clone()));
worker.register_tool(memory::tool::edit_tool(layout.clone())); worker.register_tool(memory::tool::edit_tool(layout.clone()));
@ -2735,12 +2735,12 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
let mut common = prepare_pod_common(&manifest, &loader, /* parse_template */ true)?; let mut common = prepare_pod_common(&manifest, &loader, /* parse_template */ true)?;
let skill_shadows = std::mem::take(&mut common.skill_shadows); let skill_shadows = std::mem::take(&mut common.skill_shadows);
// Session creation is deferred to the first run (see // Segment creation is deferred to the first run (see
// `ensure_session_head`) so the SessionStart entry can capture // `ensure_segment_head`) so the SegmentStart entry can capture
// the rendered system prompt, not the raw template source. The // the rendered system prompt, not the raw template source. The
// session_id is allocated here so the pod-registry registration // segment_id is allocated here so the pod-registry registration
// can record it from the start. // can record it from the start.
let session_id = session_store::new_session_id(); let segment_id = session_store::new_segment_id();
// Register this Pod in the machine-wide pod-registry // Register this Pod in the machine-wide pod-registry
// before building anything else, so a spawn that conflicts on // before building anything else, so a spawn that conflicts on
@ -2754,18 +2754,18 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
std::process::id(), std::process::id(),
socket_path, socket_path,
common.scope.allow_rules(), common.scope.allow_rules(),
session_id, segment_id,
)?; )?;
let mut worker = Worker::new(common.client); let mut worker = Worker::new(common.client);
apply_worker_manifest(&mut worker, &manifest.worker); apply_worker_manifest(&mut worker, &manifest.worker);
worker.set_cache_key(Some(session_id.to_string())); worker.set_cache_key(Some(segment_id.to_string()));
let mut pod = Self { let mut pod = Self {
manifest, manifest,
worker: Some(worker), worker: Some(worker),
store, store,
session_state: SessionState::new(session_id, 0), segment_state: SegmentState::new(segment_id, 0),
pwd: common.pwd, pwd: common.pwd,
scope: SharedScope::new(common.scope), scope: SharedScope::new(common.scope),
hook_builder: HookRegistryBuilder::new(), hook_builder: HookRegistryBuilder::new(),
@ -2793,7 +2793,7 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
extract_pointer: Arc::new(Mutex::new(None)), extract_pointer: Arc::new(Mutex::new(None)),
memory_task: None, memory_task: None,
user_segments: Vec::new(), user_segments: Vec::new(),
sink: SessionLogSink::new(), sink: SegmentLogSink::new(),
history_persistence_wired: false, history_persistence_wired: false,
log_writer: None, log_writer: None,
}; };
@ -2820,22 +2820,22 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
let mut common = prepare_pod_common(&manifest, &loader, /* parse_template */ true)?; let mut common = prepare_pod_common(&manifest, &loader, /* parse_template */ true)?;
let skill_shadows = std::mem::take(&mut common.skill_shadows); let skill_shadows = std::mem::take(&mut common.skill_shadows);
let session_id = session_store::new_session_id(); let segment_id = session_store::new_segment_id();
let scope_allocation = pod_registry::adopt_allocation( let scope_allocation = pod_registry::adopt_allocation(
manifest.pod.name.clone(), manifest.pod.name.clone(),
std::process::id(), std::process::id(),
session_id, segment_id,
)?; )?;
let mut worker = Worker::new(common.client); let mut worker = Worker::new(common.client);
apply_worker_manifest(&mut worker, &manifest.worker); apply_worker_manifest(&mut worker, &manifest.worker);
worker.set_cache_key(Some(session_id.to_string())); worker.set_cache_key(Some(segment_id.to_string()));
let mut pod = Self { let mut pod = Self {
manifest, manifest,
worker: Some(worker), worker: Some(worker),
store, store,
session_state: SessionState::new(session_id, 0), segment_state: SegmentState::new(segment_id, 0),
pwd: common.pwd, pwd: common.pwd,
scope: SharedScope::new(common.scope), scope: SharedScope::new(common.scope),
hook_builder: HookRegistryBuilder::new(), hook_builder: HookRegistryBuilder::new(),
@ -2863,7 +2863,7 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
extract_pointer: Arc::new(Mutex::new(None)), extract_pointer: Arc::new(Mutex::new(None)),
memory_task: None, memory_task: None,
user_segments: Vec::new(), user_segments: Vec::new(),
sink: SessionLogSink::new(), sink: SegmentLogSink::new(),
history_persistence_wired: false, history_persistence_wired: false,
log_writer: None, log_writer: None,
}; };
@ -2878,13 +2878,13 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
/// Resolves the manifest cascade exactly like [`Self::from_manifest`] /// Resolves the manifest cascade exactly like [`Self::from_manifest`]
/// (pwd / scope / pod-registry / client / prompt catalog), seeds a /// (pwd / scope / pod-registry / client / prompt catalog), seeds a
/// fresh Worker from the source session's `RestoredState`, and /// fresh Worker from the source session's `RestoredState`, and
/// reuses the same `session_id` so subsequent turns append to the /// reuses the same `segment_id` so subsequent turns append to the
/// source jsonl as a continuation of the same conversation. /// source jsonl as a continuation of the same conversation.
/// ///
/// Concurrent writers are prevented by the pod-registry: /// Concurrent writers are prevented by the pod-registry:
/// the registration carries `session_id`, and this constructor /// the registration carries `segment_id`, and this constructor
/// refuses to start when `pod_registry::lookup_session` already finds /// refuses to start when `pod_registry::lookup_segment` already finds
/// a live Pod writing to `session_id`. So there is no need to fork — /// a live Pod writing to `segment_id`. So there is no need to fork —
/// resume is "the same session, a different process owning it". /// resume is "the same session, a different process owning it".
/// ///
/// `system_prompt` is replayed verbatim from the session log — /// `system_prompt` is replayed verbatim from the session log —
@ -2892,7 +2892,7 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
/// session keeps a stable cache prefix even when the manifest's /// session keeps a stable cache prefix even when the manifest's
/// instruction template would render differently today. /// instruction template would render differently today.
pub async fn restore_from_manifest( pub async fn restore_from_manifest(
session_id: SessionId, segment_id: SegmentId,
manifest: PodManifest, manifest: PodManifest,
store: St, store: St,
loader: PromptLoader, loader: PromptLoader,
@ -2900,16 +2900,16 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
// Read raw entries once so we can both reconstruct state and // Read raw entries once so we can both reconstruct state and
// seed the broadcast sink's mirror with the same prefix that // seed the broadcast sink's mirror with the same prefix that
// sits on disk. // sits on disk.
let raw_entries = store.read_all(session_id)?; let raw_entries = store.read_all(segment_id)?;
let state = session_store::collect_state(&raw_entries); let state = session_store::collect_state(&raw_entries);
if state.entries_count == 0 { if state.entries_count == 0 {
return Err(PodError::SessionEmpty { session_id }); return Err(PodError::SegmentEmpty { segment_id });
} }
let mirror_entries: Vec<LogEntry> = raw_entries.clone(); let mirror_entries: Vec<LogEntry> = raw_entries.clone();
let scope_snapshot = state let scope_snapshot = state
.pod_scope .pod_scope
.clone() .clone()
.ok_or(PodError::SessionScopeMissing { session_id })?; .ok_or(PodError::SegmentScopeMissing { segment_id })?;
let mut common = prepare_pod_common_with_scope( let mut common = prepare_pod_common_with_scope(
&manifest, &manifest,
@ -2923,7 +2923,7 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
let skill_shadows = std::mem::take(&mut common.skill_shadows); let skill_shadows = std::mem::take(&mut common.skill_shadows);
// Atomic: register_pod inside install_top_level rejects when // Atomic: register_pod inside install_top_level rejects when
// another live allocation already holds `session_id`. Wrapping // another live allocation already holds `segment_id`. Wrapping
// the lookup + install inside a single `LockFileGuard` is what // the lookup + install inside a single `LockFileGuard` is what
// makes "no two live Pods write to the same session log" // makes "no two live Pods write to the same session log"
// actually structural rather than a hopeful pre-check. // actually structural rather than a hopeful pre-check.
@ -2937,14 +2937,14 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
socket_path, socket_path,
common.scope.allow_rules(), common.scope.allow_rules(),
common.scope.deny_rules(), common.scope.deny_rules(),
session_id, segment_id,
)?; )?;
// Build the worker and apply the manifest defaults first, then // Build the worker and apply the manifest defaults first, then
// overwrite the pieces the session log is authoritative for. // overwrite the pieces the session log is authoritative for.
let mut worker = Worker::new(common.client); let mut worker = Worker::new(common.client);
apply_worker_manifest(&mut worker, &manifest.worker); apply_worker_manifest(&mut worker, &manifest.worker);
worker.set_cache_key(Some(session_id.to_string())); worker.set_cache_key(Some(segment_id.to_string()));
if let Some(ref prompt) = state.system_prompt { if let Some(ref prompt) = state.system_prompt {
worker.set_system_prompt(prompt); worker.set_system_prompt(prompt);
} }
@ -2974,7 +2974,7 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
manifest, manifest,
worker: Some(worker), worker: Some(worker),
store, store,
session_state: SessionState::new(session_id, state.entries_count), segment_state: SegmentState::new(segment_id, state.entries_count),
pwd: common.pwd, pwd: common.pwd,
scope: SharedScope::new(common.scope), scope: SharedScope::new(common.scope),
hook_builder: HookRegistryBuilder::new(), hook_builder: HookRegistryBuilder::new(),
@ -3007,7 +3007,7 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
// Seed the mirror with the entries we just replayed so a // Seed the mirror with the entries we just replayed so a
// late-attaching client sees the full prefix without an // late-attaching client sees the full prefix without an
// extra round trip. // extra round trip.
sink: SessionLogSink::with_initial(mirror_entries), sink: SegmentLogSink::with_initial(mirror_entries),
history_persistence_wired: false, history_persistence_wired: false,
log_writer: None, log_writer: None,
}; };
@ -3234,13 +3234,13 @@ pub enum PodError {
#[error("workflow invocation failed: {0}")] #[error("workflow invocation failed: {0}")]
WorkflowResolve(#[from] WorkflowResolveError), WorkflowResolve(#[from] WorkflowResolveError),
#[error("session {session_id} has no entries to restore")] #[error("session {segment_id} has no entries to restore")]
SessionEmpty { session_id: SessionId }, SegmentEmpty { segment_id: SegmentId },
#[error( #[error(
"session {session_id} has no persisted scope snapshot; refusing resume without explicit scope" "session {segment_id} has no persisted scope snapshot; refusing resume without explicit scope"
)] )]
SessionScopeMissing { session_id: SessionId }, SegmentScopeMissing { segment_id: SegmentId },
} }
/// Bundle of resources that every high-level Pod constructor needs: /// Bundle of resources that every high-level Pod constructor needs:

View File

@ -131,7 +131,7 @@ mod tests {
fn test_state() -> PodSharedState { fn test_state() -> PodSharedState {
PodSharedState::new( PodSharedState::new(
"test-pod".into(), "test-pod".into(),
session_store::new_session_id(), session_store::new_segment_id(),
"[pod]\nname = \"test-pod\"".into(), "[pod]\nname = \"test-pod\"".into(),
protocol::Greeting { protocol::Greeting {
pod_name: "test-pod".into(), pod_name: "test-pod".into(),

View File

@ -10,11 +10,11 @@
//! Atomicity contract: //! Atomicity contract:
//! //!
//! 1. Pod writes the entry to disk via the `Store`. //! 1. Pod writes the entry to disk via the `Store`.
//! 2. Pod calls [`SessionLogSink::publish`] which acquires the mirror //! 2. Pod calls [`SegmentLogSink::publish`] which acquires the mirror
//! mutex, pushes the entry, and fires `broadcast::send` — all under //! mutex, pushes the entry, and fires `broadcast::send` — all under
//! the same critical section. //! the same critical section.
//! //!
//! [`SessionLogSink::subscribe_with_snapshot`] takes the same mutex, //! [`SegmentLogSink::subscribe_with_snapshot`] takes the same mutex,
//! so the `(snapshot, receiver)` pair returned to a connecting client //! so the `(snapshot, receiver)` pair returned to a connecting client
//! splits the entry sequence cleanly: every entry shows up in exactly //! splits the entry sequence cleanly: every entry shows up in exactly
//! one of `snapshot` or on `receiver`. //! one of `snapshot` or on `receiver`.
@ -39,24 +39,24 @@ const BROADCAST_CAPACITY: usize = 256;
/// for read-only `subscribe_with_snapshot` access and keeps one for /// for read-only `subscribe_with_snapshot` access and keeps one for
/// its own write path. /// its own write path.
#[derive(Clone)] #[derive(Clone)]
pub struct SessionLogSink { pub struct SegmentLogSink {
inner: Arc<SinkInner>, inner: Arc<SinkInner>,
} }
struct SinkInner { struct SinkInner {
/// Full session log mirror in commit order. Reset on session swap /// Full session log mirror in commit order. Reset on session swap
/// (compaction / fork) via [`SessionLogSink::reset_with_initial`]. /// (compaction / fork) via [`SegmentLogSink::reset_with_initial`].
mirror: StdMutex<Vec<LogEntry>>, mirror: StdMutex<Vec<LogEntry>>,
/// Broadcast channel for live entry updates. The same `Sender` /// Broadcast channel for live entry updates. The same `Sender`
/// survives session swaps so existing subscribers keep their /// survives session swaps so existing subscribers keep their
/// receiver — they observe the swap as a freshly broadcast /// receiver — they observe the swap as a freshly broadcast
/// `LogEntry::SessionStart` and reset their view accordingly. /// `LogEntry::SegmentStart` and reset their view accordingly.
broadcast_tx: broadcast::Sender<LogEntry>, broadcast_tx: broadcast::Sender<LogEntry>,
} }
impl SessionLogSink { impl SegmentLogSink {
/// Create a fresh sink with an empty mirror. Used before any entry /// Create a fresh sink with an empty mirror. Used before any entry
/// has been written (deferred SessionStart) or as a placeholder in /// has been written (deferred SegmentStart) or as a placeholder in
/// tests. /// tests.
pub fn new() -> Self { pub fn new() -> Self {
let (broadcast_tx, _) = broadcast::channel(BROADCAST_CAPACITY); let (broadcast_tx, _) = broadcast::channel(BROADCAST_CAPACITY);
@ -89,7 +89,7 @@ impl SessionLogSink {
/// ///
/// Live broadcast fires only for entries that the streaming-event /// Live broadcast fires only for entries that the streaming-event
/// lane does not cover: /// lane does not cover:
/// - `LogEntry::SessionStart` → `Event::SessionRotated` on the wire. /// - `LogEntry::SegmentStart` → `Event::SegmentRotated` on the wire.
/// - `LogEntry::SystemItem` → `Event::SystemItem`. /// - `LogEntry::SystemItem` → `Event::SystemItem`.
/// - `LogEntry::Invoke` → `Event::InvokeStart`. /// - `LogEntry::Invoke` → `Event::InvokeStart`.
/// Everything else (AssistantItem, ToolResult, UserInput, TurnEnd, /// Everything else (AssistantItem, ToolResult, UserInput, TurnEnd,
@ -119,7 +119,7 @@ impl SessionLogSink {
fn is_live_relevant(entry: &LogEntry) -> bool { fn is_live_relevant(entry: &LogEntry) -> bool {
matches!( matches!(
entry, entry,
LogEntry::SessionStart { .. } LogEntry::SegmentStart { .. }
| LogEntry::SystemItem { .. } | LogEntry::SystemItem { .. }
| LogEntry::Invoke { .. } | LogEntry::Invoke { .. }
) )
@ -127,12 +127,12 @@ impl SessionLogSink {
/// Atomically swap the mirror to `[initial]` and broadcast the new /// Atomically swap the mirror to `[initial]` and broadcast the new
/// session-start entry. Used during compaction / fork: the new /// session-start entry. Used during compaction / fork: the new
/// `LogEntry::SessionStart` is the first entry of the replacement /// `LogEntry::SegmentStart` is the first entry of the replacement
/// session, and existing subscribers transition by replaying it /// session, and existing subscribers transition by replaying it
/// like any other live entry. /// like any other live entry.
/// ///
/// Existing snapshot prefixes seen by old subscribers stay valid /// Existing snapshot prefixes seen by old subscribers stay valid
/// for the prior session; the new `SessionStart` on the broadcast /// for the prior session; the new `SegmentStart` on the broadcast
/// is the signal to reset their derived view. /// is the signal to reset their derived view.
pub fn reset_with_initial(&self, initial: LogEntry) { pub fn reset_with_initial(&self, initial: LogEntry) {
let mut mirror = self let mut mirror = self
@ -188,7 +188,7 @@ impl SessionLogSink {
} }
} }
impl Default for SessionLogSink { impl Default for SegmentLogSink {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()
} }
@ -198,10 +198,10 @@ impl Default for SessionLogSink {
mod tests { mod tests {
use super::*; use super::*;
use llm_worker::llm_client::RequestConfig; use llm_worker::llm_client::RequestConfig;
use session_store::session_log::now_millis; use session_store::segment_log::now_millis;
fn session_start() -> LogEntry { fn session_start() -> LogEntry {
LogEntry::SessionStart { LogEntry::SegmentStart {
ts: now_millis(), ts: now_millis(),
system_prompt: None, system_prompt: None,
config: RequestConfig::default(), config: RequestConfig::default(),
@ -220,13 +220,13 @@ mod tests {
#[test] #[test]
fn publish_then_subscribe_returns_history_in_snapshot() { fn publish_then_subscribe_returns_history_in_snapshot() {
let sink = SessionLogSink::new(); let sink = SegmentLogSink::new();
sink.publish(session_start()); sink.publish(session_start());
sink.publish(turn_end(1)); sink.publish(turn_end(1));
let (snapshot, mut rx) = sink.subscribe_with_snapshot(); let (snapshot, mut rx) = sink.subscribe_with_snapshot();
assert_eq!(snapshot.len(), 2); assert_eq!(snapshot.len(), 2);
assert!(matches!(snapshot[0], LogEntry::SessionStart { .. })); assert!(matches!(snapshot[0], LogEntry::SegmentStart { .. }));
assert!(matches!( assert!(matches!(
snapshot[1], snapshot[1],
LogEntry::TurnEnd { turn_count: 1, .. } LogEntry::TurnEnd { turn_count: 1, .. }
@ -246,7 +246,7 @@ mod tests {
#[test] #[test]
fn subscribe_then_publish_delivers_only_live_relevant_entries() { fn subscribe_then_publish_delivers_only_live_relevant_entries() {
let sink = SessionLogSink::new(); let sink = SegmentLogSink::new();
sink.publish(session_start()); sink.publish(session_start());
let (snapshot, mut rx) = sink.subscribe_with_snapshot(); let (snapshot, mut rx) = sink.subscribe_with_snapshot();
@ -270,7 +270,7 @@ mod tests {
#[test] #[test]
fn snapshot_and_live_never_overlap() { fn snapshot_and_live_never_overlap() {
let sink = SessionLogSink::new(); let sink = SegmentLogSink::new();
sink.publish(session_start()); sink.publish(session_start());
let (snapshot, mut rx) = sink.subscribe_with_snapshot(); let (snapshot, mut rx) = sink.subscribe_with_snapshot();
sink.publish(notification_entry("post-snapshot")); sink.publish(notification_entry("post-snapshot"));
@ -285,7 +285,7 @@ mod tests {
#[test] #[test]
fn reset_with_initial_clears_and_broadcasts() { fn reset_with_initial_clears_and_broadcasts() {
let sink = SessionLogSink::new(); let sink = SegmentLogSink::new();
sink.publish(session_start()); sink.publish(session_start());
sink.publish(turn_end(1)); sink.publish(turn_end(1));
@ -293,18 +293,18 @@ mod tests {
sink.reset_with_initial(session_start()); sink.reset_with_initial(session_start());
match rx.try_recv() { match rx.try_recv() {
Ok(LogEntry::SessionStart { .. }) => {} Ok(LogEntry::SegmentStart { .. }) => {}
other => panic!("expected SessionStart broadcast, got {other:?}"), other => panic!("expected SegmentStart broadcast, got {other:?}"),
} }
let (post_snapshot, _) = sink.subscribe_with_snapshot(); let (post_snapshot, _) = sink.subscribe_with_snapshot();
assert_eq!(post_snapshot.len(), 1); assert_eq!(post_snapshot.len(), 1);
assert!(matches!(post_snapshot[0], LogEntry::SessionStart { .. })); assert!(matches!(post_snapshot[0], LogEntry::SegmentStart { .. }));
} }
#[test] #[test]
fn replace_silent_does_not_broadcast() { fn replace_silent_does_not_broadcast() {
let sink = SessionLogSink::new(); let sink = SegmentLogSink::new();
sink.publish(session_start()); sink.publish(session_start());
let (_pre_snapshot, mut rx) = sink.subscribe_with_snapshot(); let (_pre_snapshot, mut rx) = sink.subscribe_with_snapshot();
@ -318,7 +318,7 @@ mod tests {
#[test] #[test]
fn with_initial_seeds_the_mirror() { fn with_initial_seeds_the_mirror() {
let sink = SessionLogSink::with_initial(vec![session_start(), turn_end(1)]); let sink = SegmentLogSink::with_initial(vec![session_start(), turn_end(1)]);
let (snapshot, _) = sink.subscribe_with_snapshot(); let (snapshot, _) = sink.subscribe_with_snapshot();
assert_eq!(snapshot.len(), 2); assert_eq!(snapshot.len(), 2);
} }

View File

@ -2,7 +2,7 @@ use std::sync::{OnceLock, RwLock};
use protocol::PodStatus; use protocol::PodStatus;
use serde_json::json; use serde_json::json;
use session_store::SessionId; use session_store::SegmentId;
use crate::fs_view::PodFsView; use crate::fs_view::PodFsView;
@ -28,7 +28,7 @@ pub struct KnowledgeCandidate {
/// greeting, and completion lookup hubs. /// greeting, and completion lookup hubs.
pub struct PodSharedState { pub struct PodSharedState {
pub pod_name: String, pub pod_name: String,
pub session_id: SessionId, pub segment_id: SegmentId,
pub manifest_toml: String, pub manifest_toml: String,
pub greeting: protocol::Greeting, pub greeting: protocol::Greeting,
pub status: RwLock<PodStatus>, pub status: RwLock<PodStatus>,
@ -46,13 +46,13 @@ pub struct PodSharedState {
impl PodSharedState { impl PodSharedState {
pub fn new( pub fn new(
pod_name: String, pod_name: String,
session_id: SessionId, segment_id: SegmentId,
manifest_toml: String, manifest_toml: String,
greeting: protocol::Greeting, greeting: protocol::Greeting,
) -> Self { ) -> Self {
Self { Self {
pod_name, pod_name,
session_id, segment_id,
manifest_toml, manifest_toml,
greeting, greeting,
status: RwLock::new(PodStatus::Idle), status: RwLock::new(PodStatus::Idle),
@ -123,7 +123,7 @@ impl PodSharedState {
let status = self.get_status(); let status = self.get_status();
json!({ json!({
"state": status, "state": status,
"session_id": self.session_id.to_string(), "segment_id": self.segment_id.to_string(),
"pod_name": self.pod_name, "pod_name": self.pod_name,
}) })
.to_string() .to_string()
@ -137,7 +137,7 @@ mod tests {
fn test_state() -> PodSharedState { fn test_state() -> PodSharedState {
PodSharedState::new( PodSharedState::new(
"test-pod".into(), "test-pod".into(),
session_store::new_session_id(), session_store::new_segment_id(),
"[pod]\nname = \"test-pod\"".into(), "[pod]\nname = \"test-pod\"".into(),
test_greeting(), test_greeting(),
) )
@ -176,7 +176,7 @@ mod tests {
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["state"], "idle"); assert_eq!(parsed["state"], "idle");
assert_eq!(parsed["pod_name"], "test-pod"); assert_eq!(parsed["pod_name"], "test-pod");
assert!(parsed["session_id"].is_string()); assert!(parsed["segment_id"].is_string());
} }
#[test] #[test]

View File

@ -430,13 +430,13 @@ fn extract_assistant_text(entries: &[serde_json::Value]) -> String {
for value in entries { for value in entries {
// The wire payload is the JSON form of `session_store::LogEntry`. // The wire payload is the JSON form of `session_store::LogEntry`.
// Walk Assistant items inside each entry that can carry them: // Walk Assistant items inside each entry that can carry them:
// post-compaction `SessionStart.history` (seed) and per-LLM-call // post-compaction `SegmentStart.history` (seed) and per-LLM-call
// `AssistantItems` deltas. // `AssistantItems` deltas.
let Ok(entry) = serde_json::from_value::<LogEntry>(value.clone()) else { let Ok(entry) = serde_json::from_value::<LogEntry>(value.clone()) else {
continue; continue;
}; };
let logged_items = match entry { let logged_items = match entry {
LogEntry::SessionStart { history, .. } => history, LogEntry::SegmentStart { history, .. } => history,
LogEntry::AssistantItems { items, .. } => items, LogEntry::AssistantItems { items, .. } => items,
_ => continue, _ => continue,
}; };

View File

@ -482,7 +482,7 @@ fn pod_registry_err_to_tool(e: ScopeLockError) -> ToolError {
| ScopeLockError::WriteConflict { .. } | ScopeLockError::WriteConflict { .. }
| ScopeLockError::DuplicatePodName(_) | ScopeLockError::DuplicatePodName(_)
| ScopeLockError::UnknownPod(_) | ScopeLockError::UnknownPod(_)
| ScopeLockError::SessionConflict { .. } => ToolError::InvalidArgument(e.to_string()), | ScopeLockError::SegmentConflict { .. } => ToolError::InvalidArgument(e.to_string()),
ScopeLockError::Io(_) => ToolError::ExecutionFailed(e.to_string()), ScopeLockError::Io(_) => ToolError::ExecutionFailed(e.to_string()),
} }
} }

View File

@ -178,7 +178,7 @@ fn drain(rx: &mut broadcast::Receiver<Event>) -> Vec<Event> {
} }
/// Collect every system-message text that the post-compaction /// Collect every system-message text that the post-compaction
/// `SessionStart.history` carries, by reading the sink mirror directly. /// `SegmentStart.history` carries, by reading the sink mirror directly.
fn system_texts_in_sink_session_start( fn system_texts_in_sink_session_start(
pod: &pod::Pod< pod: &pod::Pod<
impl llm_worker::llm_client::client::LlmClient + Clone + 'static, impl llm_worker::llm_client::client::LlmClient + Clone + 'static,
@ -187,7 +187,7 @@ fn system_texts_in_sink_session_start(
) -> Vec<String> { ) -> Vec<String> {
let (entries, _rx) = pod.sink().subscribe_with_snapshot(); let (entries, _rx) = pod.sink().subscribe_with_snapshot();
for entry in entries.into_iter().rev() { for entry in entries.into_iter().rev() {
if let session_store::LogEntry::SessionStart { history, .. } = entry { if let session_store::LogEntry::SegmentStart { history, .. } = entry {
return history return history
.into_iter() .into_iter()
.filter_map(|logged| { .filter_map(|logged| {
@ -229,7 +229,7 @@ async fn compact_emits_session_start_carrying_summary_and_task_snapshot() {
pod.compact(10_000).await.unwrap(); pod.compact(10_000).await.unwrap();
let system_texts = system_texts_in_sink_session_start(&pod); let system_texts = system_texts_in_sink_session_start(&pod);
// The post-compaction `SessionStart.history` carries the new system // The post-compaction `SegmentStart.history` carries the new system
// messages introduced by the compactor. Clients re-seed their view // messages introduced by the compactor. Clients re-seed their view
// from this entry alone, so it is the load-bearing payload. // from this entry alone, so it is the load-bearing payload.
assert!( assert!(
@ -289,11 +289,11 @@ async fn pre_run_compact_success_broadcasts_start_and_done() {
// CompactDone carries the new session id. // CompactDone carries the new session id.
let new_id_in_event = events.iter().find_map(|e| match e { let new_id_in_event = events.iter().find_map(|e| match e {
Event::CompactDone { new_session_id } => Some(*new_session_id), Event::CompactDone { new_segment_id } => Some(*new_segment_id),
_ => None, _ => None,
}); });
assert!(new_id_in_event.is_some(), "CompactDone missing"); assert!(new_id_in_event.is_some(), "CompactDone missing");
assert_eq!(new_id_in_event.unwrap(), pod.session_id()); assert_eq!(new_id_in_event.unwrap(), pod.segment_id());
} }
#[tokio::test] #[tokio::test]
@ -345,10 +345,10 @@ async fn mid_turn_compact_success_broadcasts_start_and_done() {
); );
let new_id_in_event = events.iter().find_map(|e| match e { let new_id_in_event = events.iter().find_map(|e| match e {
Event::CompactDone { new_session_id } => Some(*new_session_id), Event::CompactDone { new_segment_id } => Some(*new_segment_id),
_ => None, _ => None,
}); });
assert_eq!(new_id_in_event, Some(pod.session_id())); assert_eq!(new_id_in_event, Some(pod.segment_id()));
} }
/// Regression: `Pod::compact()` must reset the in-memory /// Regression: `Pod::compact()` must reset the in-memory
@ -520,7 +520,7 @@ async fn pre_run_compact_failure_broadcasts_start_and_failed() {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Detached post-run memory jobs (`spawn_post_run_memory_jobs` / // Detached post-run memory jobs (`spawn_post_run_memory_jobs` /
// `wait_for_memory_jobs`). Covers the detach round-trip and the structural // `wait_for_memory_jobs`). Covers the detach round-trip and the structural
// invariant that the cloned memory-task Pod shares `SessionState` with the // invariant that the cloned memory-task Pod shares `SegmentState` with the
// source Pod, so that `save_extension` from the background extract does not // source Pod, so that `save_extension` from the background extract does not
// leave the next turn's `save_user_input` looking at a stale session pointer. // leave the next turn's `save_user_input` looking at a stale session pointer.
@ -570,7 +570,7 @@ async fn spawn_and_wait_drives_extract_to_completion() {
#[tokio::test] #[tokio::test]
async fn detached_extract_does_not_fork_session_log() { async fn detached_extract_does_not_fork_session_log() {
// Source pod and the cloned memory-task pod share `SessionState` via // Source pod and the cloned memory-task pod share `SegmentState` via
// `Arc<_>`. The detached extract advances the entry tally through // `Arc<_>`. The detached extract advances the entry tally through
// `save_extension`; the next `run` must see that same tally so // `save_extension`; the next `run` must see that same tally so
// `ensure_head_or_fork` does not spawn a new session. // `ensure_head_or_fork` does not spawn a new session.
@ -583,18 +583,18 @@ async fn detached_extract_does_not_fork_session_log() {
let mut pod = make_pod_with_manifest(EXTRACT_NO_COMPACT_MANIFEST, client).await; let mut pod = make_pod_with_manifest(EXTRACT_NO_COMPACT_MANIFEST, client).await;
pod.run_text("first").await.unwrap(); pod.run_text("first").await.unwrap();
let session_before = pod.session_id(); let session_before = pod.segment_id();
pod.spawn_post_run_memory_jobs(); pod.spawn_post_run_memory_jobs();
pod.wait_for_memory_jobs().await; pod.wait_for_memory_jobs().await;
pod.run_text("second").await.unwrap(); pod.run_text("second").await.unwrap();
let session_after = pod.session_id(); let session_after = pod.segment_id();
assert_eq!( assert_eq!(
session_before, session_after, session_before, session_after,
"detached extract's save_extension and the next turn's save_user_input \ "detached extract's save_extension and the next turn's save_user_input \
must share the entry tally through SessionState a fork here means the \ must share the entry tally through SegmentState a fork here means the \
clone carried its own counter" clone carried its own counter"
); );
} }

View File

@ -172,7 +172,7 @@ fn write_n_staging(layout: &WorkspaceLayout, n: usize) -> Vec<uuid::Uuid> {
let (id, _) = write_staging( let (id, _) = write_staging(
layout, layout,
SourceRef { SourceRef {
session_id: format!("s-{i}"), segment_id: format!("s-{i}"),
range: [i as u64, i as u64], range: [i as u64, i as u64],
}, },
ExtractedPayload::default(), ExtractedPayload::default(),

View File

@ -22,7 +22,7 @@ fn history_from_sink(handle: &PodHandle) -> Vec<Item> {
let mut items = Vec::new(); let mut items = Vec::new();
for entry in entries { for entry in entries {
match entry { match entry {
LogEntry::SessionStart { history, .. } => { LogEntry::SegmentStart { history, .. } => {
items.extend(history.into_iter().map(Item::from)); items.extend(history.into_iter().map(Item::from));
} }
LogEntry::UserInput { segments, .. } => { LogEntry::UserInput { segments, .. } => {

View File

@ -349,7 +349,7 @@ async fn stop_pod_sends_shutdown_and_releases_scope() {
permission: Permission::Write, permission: Permission::Write,
recursive: true, recursive: true,
}], }],
session_store::new_session_id(), session_store::new_segment_id(),
) )
.unwrap(); .unwrap();
pod_registry::delegate_scope( pod_registry::delegate_scope(

View File

@ -358,7 +358,7 @@ async fn shutdown_releases_scope_allocation_when_present() {
std::process::id(), std::process::id(),
"/tmp/kid.sock".into(), "/tmp/kid.sock".into(),
vec![], vec![],
session_store::new_session_id(), session_store::new_segment_id(),
) )
.unwrap(); .unwrap();
std::mem::forget(guard); std::mem::forget(guard);

View File

@ -8,7 +8,7 @@
use std::sync::{LazyLock, Mutex}; use std::sync::{LazyLock, Mutex};
use pod::{Pod, PodError}; use pod::{Pod, PodError};
use session_store::{FsStore, SessionId, StoreError}; use session_store::{FsStore, SegmentId, StoreError};
const MINIMAL_MANIFEST_TOML: &str = r#" const MINIMAL_MANIFEST_TOML: &str = r#"
[pod] [pod]
@ -42,7 +42,7 @@ async fn restore_from_manifest_rejects_unknown_session() {
// A freshly-minted id with no jsonl file at all → store returns // A freshly-minted id with no jsonl file at all → store returns
// NotFound, which `Pod::restore_from_manifest` surfaces verbatim // NotFound, which `Pod::restore_from_manifest` surfaces verbatim
// as `PodError::Store`. // as `PodError::Store`.
let unknown = session_store::new_session_id(); let unknown = session_store::new_segment_id();
let result = let result =
Pod::restore_from_manifest(unknown, manifest, store, pod::PromptLoader::builtins_only()) Pod::restore_from_manifest(unknown, manifest, store, pod::PromptLoader::builtins_only())
.await; .await;
@ -64,10 +64,10 @@ async fn restore_from_manifest_rejects_empty_session_log() {
// Pre-create an empty `<id>.jsonl` so `read_all` succeeds with no // Pre-create an empty `<id>.jsonl` so `read_all` succeeds with no
// entries. `collect_state` returns `entries_count = 0`, which // entries. `collect_state` returns `entries_count = 0`, which
// `restore_from_manifest` rejects with `SessionEmpty` *before* it // `restore_from_manifest` rejects with `SegmentEmpty` *before* it
// gets as far as building the LLM client — so the test does not // gets as far as building the LLM client — so the test does not
// need credentials or a runtime sandbox. // need credentials or a runtime sandbox.
let id: SessionId = session_store::new_session_id(); let id: SegmentId = session_store::new_segment_id();
let path = store_tmp.path().join(format!("{id}.jsonl")); let path = store_tmp.path().join(format!("{id}.jsonl"));
std::fs::write(&path, b"").unwrap(); std::fs::write(&path, b"").unwrap();
@ -75,8 +75,8 @@ async fn restore_from_manifest_rejects_empty_session_log() {
Pod::restore_from_manifest(id, manifest, store, pod::PromptLoader::builtins_only()).await; Pod::restore_from_manifest(id, manifest, store, pod::PromptLoader::builtins_only()).await;
match result { match result {
Err(PodError::SessionEmpty { session_id }) => assert_eq!(session_id, id), Err(PodError::SegmentEmpty { segment_id }) => assert_eq!(segment_id, id),
Err(other) => panic!("expected SessionEmpty, got {other:?}"), Err(other) => panic!("expected SegmentEmpty, got {other:?}"),
Ok(_) => panic!("expected empty session log to fail"), Ok(_) => panic!("expected empty session log to fail"),
} }
} }
@ -89,20 +89,20 @@ async fn restore_from_manifest_rejects_session_without_scope_snapshot() {
let store = FsStore::new(store_tmp.path()).unwrap(); let store = FsStore::new(store_tmp.path()).unwrap();
let manifest = pod::PodManifest::from_toml(MINIMAL_MANIFEST_TOML).unwrap(); let manifest = pod::PodManifest::from_toml(MINIMAL_MANIFEST_TOML).unwrap();
let id = session_store::new_session_id(); let id = session_store::new_segment_id();
let state = session_store::SessionStartState { let state = session_store::SegmentStartState {
system_prompt: None, system_prompt: None,
config: &Default::default(), config: &Default::default(),
history: &[], history: &[],
}; };
session_store::create_session_with_id(&store, id, state).unwrap(); session_store::create_segment_with_id(&store, id, state).unwrap();
let result = let result =
Pod::restore_from_manifest(id, manifest, store, pod::PromptLoader::builtins_only()).await; Pod::restore_from_manifest(id, manifest, store, pod::PromptLoader::builtins_only()).await;
match result { match result {
Err(PodError::SessionScopeMissing { session_id }) => assert_eq!(session_id, id), Err(PodError::SegmentScopeMissing { segment_id }) => assert_eq!(segment_id, id),
Err(other) => panic!("expected SessionScopeMissing, got {other:?}"), Err(other) => panic!("expected SegmentScopeMissing, got {other:?}"),
Ok(_) => panic!("expected missing scope snapshot to fail"), Ok(_) => panic!("expected missing scope snapshot to fail"),
} }
} }

View File

@ -26,7 +26,7 @@ use llm_worker::llm_client::event::{Event as LlmEvent, ResponseStatus, StatusEve
use llm_worker::llm_client::{ClientError, LlmClient, Request}; use llm_worker::llm_client::{ClientError, LlmClient, Request};
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput}; use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
use session_metrics::{DOMAIN, Metric, metrics_from_extensions}; use session_metrics::{DOMAIN, Metric, metrics_from_extensions};
use session_store::{FsStore, LogEntry, SessionId, Store, StoreError, TraceEntry}; use session_store::{FsStore, LogEntry, SegmentId, Store, StoreError, TraceEntry};
use pod::{Pod, PodManifest}; use pod::{Pod, PodManifest};
@ -200,7 +200,7 @@ async fn prune_metrics_emit_skip_then_fire_with_post_request_join() {
text_response_with_cache("done", 1234, 50), text_response_with_cache("done", 1234, 50),
]); ]);
let (mut pod, _store_tmp, _pwd_tmp) = make_pod(manifest_toml(1, 1), client, "big_tool").await; let (mut pod, _store_tmp, _pwd_tmp) = make_pod(manifest_toml(1, 1), client, "big_tool").await;
let session_id = pod.session_id(); let segment_id = pod.segment_id();
// Cloning the store handle to read the session log back after the // Cloning the store handle to read the session log back after the
// runs complete — the Pod retains its own copy. // runs complete — the Pod retains its own copy.
let store = pod.store().clone(); let store = pod.store().clone();
@ -208,7 +208,7 @@ async fn prune_metrics_emit_skip_then_fire_with_post_request_join() {
pod.run_text("first").await.unwrap(); pod.run_text("first").await.unwrap();
pod.run_text("second").await.unwrap(); pod.run_text("second").await.unwrap();
let state = session_store::restore(&store, session_id).unwrap(); let state = session_store::restore(&store, segment_id).unwrap();
let metrics = metrics_from_extensions(&state.extensions); let metrics = metrics_from_extensions(&state.extensions);
// Run 1 has 2 LLM iterations (tool loop), each evaluates prune with // Run 1 has 2 LLM iterations (tool loop), each evaluates prune with
@ -288,13 +288,13 @@ async fn prune_metrics_record_below_min_savings_skip() {
]); ]);
let (mut pod, _store_tmp, _pwd_tmp) = let (mut pod, _store_tmp, _pwd_tmp) =
make_pod(manifest_toml(1, u64::MAX), client, "big_tool").await; make_pod(manifest_toml(1, u64::MAX), client, "big_tool").await;
let session_id = pod.session_id(); let segment_id = pod.segment_id();
let store = pod.store().clone(); let store = pod.store().clone();
pod.run_text("first").await.unwrap(); pod.run_text("first").await.unwrap();
pod.run_text("second").await.unwrap(); pod.run_text("second").await.unwrap();
let state = session_store::restore(&store, session_id).unwrap(); let state = session_store::restore(&store, segment_id).unwrap();
let metrics = metrics_from_extensions(&state.extensions); let metrics = metrics_from_extensions(&state.extensions);
let below = metrics let below = metrics
.iter() .iter()
@ -327,7 +327,7 @@ struct MetricFailingStore {
} }
impl Store for MetricFailingStore { impl Store for MetricFailingStore {
fn append(&self, id: SessionId, entry: &LogEntry) -> Result<(), StoreError> { fn append(&self, id: SegmentId, entry: &LogEntry) -> Result<(), StoreError> {
if let LogEntry::Extension { domain, .. } = entry { if let LogEntry::Extension { domain, .. } = entry {
if domain == DOMAIN { if domain == DOMAIN {
return Err(StoreError::Io(std::io::Error::other("synthetic failure"))); return Err(StoreError::Io(std::io::Error::other("synthetic failure")));
@ -335,22 +335,22 @@ impl Store for MetricFailingStore {
} }
self.inner.append(id, entry) self.inner.append(id, entry)
} }
fn read_all(&self, id: SessionId) -> Result<Vec<LogEntry>, StoreError> { fn read_all(&self, id: SegmentId) -> Result<Vec<LogEntry>, StoreError> {
self.inner.read_all(id) self.inner.read_all(id)
} }
fn list_sessions(&self) -> Result<Vec<SessionId>, StoreError> { fn list_segments(&self) -> Result<Vec<SegmentId>, StoreError> {
self.inner.list_sessions() self.inner.list_segments()
} }
fn create_session(&self, id: SessionId, entries: &[LogEntry]) -> Result<(), StoreError> { fn create_segment(&self, id: SegmentId, entries: &[LogEntry]) -> Result<(), StoreError> {
self.inner.create_session(id, entries) self.inner.create_segment(id, entries)
} }
fn exists(&self, id: SessionId) -> Result<bool, StoreError> { fn exists(&self, id: SegmentId) -> Result<bool, StoreError> {
self.inner.exists(id) self.inner.exists(id)
} }
fn read_entry_count(&self, id: SessionId) -> Result<usize, StoreError> { fn read_entry_count(&self, id: SegmentId) -> Result<usize, StoreError> {
self.inner.read_entry_count(id) self.inner.read_entry_count(id)
} }
fn append_trace(&self, id: SessionId, entry: &TraceEntry) -> Result<(), StoreError> { fn append_trace(&self, id: SegmentId, entry: &TraceEntry) -> Result<(), StoreError> {
self.inner.append_trace(id, entry) self.inner.append_trace(id, entry)
} }
} }
@ -386,12 +386,12 @@ async fn metric_write_failure_emits_warn_alert_and_does_not_abort_run() {
let alerter = pod::Alerter::new(tx); let alerter = pod::Alerter::new(tx);
pod.attach_alerter(alerter); pod.attach_alerter(alerter);
let session_id = pod.session_id(); let segment_id = pod.segment_id();
// Run completes successfully despite metric failure. // Run completes successfully despite metric failure.
pod.run_text("hello").await.unwrap(); pod.run_text("hello").await.unwrap();
// No metrics ended up in the log (writes were rejected). // No metrics ended up in the log (writes were rejected).
let state = session_store::restore(&store, session_id).unwrap(); let state = session_store::restore(&store, segment_id).unwrap();
let metrics = metrics_from_extensions(&state.extensions); let metrics = metrics_from_extensions(&state.extensions);
assert!(metrics.is_empty(), "metrics must drop on write failure"); assert!(metrics.is_empty(), "metrics must drop on write failure");
@ -446,10 +446,10 @@ permission = "write"
let mut pod = Pod::new(manifest, worker, store.clone(), pwd, scope) let mut pod = Pod::new(manifest, worker, store.clone(), pwd, scope)
.await .await
.unwrap(); .unwrap();
let session_id = pod.session_id(); let segment_id = pod.segment_id();
pod.run_text("hello").await.unwrap(); pod.run_text("hello").await.unwrap();
let state = session_store::restore(&store, session_id).unwrap(); let state = session_store::restore(&store, segment_id).unwrap();
let metrics = metrics_from_extensions(&state.extensions); let metrics = metrics_from_extensions(&state.extensions);
assert!( assert!(
metrics.is_empty(), metrics.is_empty(),

View File

@ -73,7 +73,7 @@ async fn setup_spawner(
permission: Permission::Write, permission: Permission::Write,
recursive: true, recursive: true,
}], }],
session_store::new_session_id(), session_store::new_segment_id(),
) )
.unwrap(); .unwrap();
// Leak the guard — the spawner allocation needs to outlive the // Leak the guard — the spawner allocation needs to outlive the

View File

@ -182,16 +182,16 @@ async fn session_start_state_captures_rendered_prompt() {
.unwrap(); .unwrap();
pod.run_text("hi").await.unwrap(); pod.run_text("hi").await.unwrap();
let entries = pod.store().read_all(pod.session_id()).unwrap(); let entries = pod.store().read_all(pod.segment_id()).unwrap();
let first = entries.first().expect("at least one entry"); let first = entries.first().expect("at least one entry");
match first { match first {
LogEntry::SessionStart { system_prompt, .. } => { LogEntry::SegmentStart { system_prompt, .. } => {
let sp = system_prompt.as_deref().expect("system prompt set"); let sp = system_prompt.as_deref().expect("system prompt set");
assert!(sp.starts_with("hello cwd=")); assert!(sp.starts_with("hello cwd="));
assert!(sp.contains(&pwd.display().to_string())); assert!(sp.contains(&pwd.display().to_string()));
assert!(sp.contains("## Working boundaries")); assert!(sp.contains("## Working boundaries"));
} }
other => panic!("expected SessionStart as first entry, got {other:?}"), other => panic!("expected SegmentStart as first entry, got {other:?}"),
} }
} }

View File

@ -364,7 +364,7 @@ pub enum Event {
/// Live updates after the snapshot arrive through the streaming /// Live updates after the snapshot arrive through the streaming
/// events (`TextDelta` / `ToolCall*` / `ToolResult` / etc.) plus /// events (`TextDelta` / `ToolCall*` / `ToolResult` / etc.) plus
/// the two role-specific entry events /// the two role-specific entry events
/// (`SessionRotated` / `HookInjectedItems`) — there is no generic /// (`SegmentRotated` / `HookInjectedItems`) — there is no generic
/// "every committed entry" broadcast. /// "every committed entry" broadcast.
Snapshot { Snapshot {
entries: Vec<serde_json::Value>, entries: Vec<serde_json::Value>,
@ -372,15 +372,15 @@ pub enum Event {
#[serde(default)] #[serde(default)]
status: PodStatus, status: PodStatus,
}, },
/// Server-side session log rotated to a fresh `SessionStart`. /// Server-side segment log rotated to a fresh `SegmentStart`.
/// ///
/// Fires on compaction and on auto-fork when the store head drifts /// Fires on compaction and on auto-fork when the store head drifts
/// from the live writer's cached head. Clients drop their derived /// from the live writer's cached head. Clients drop their derived
/// view and reseed from `entry.history` exactly the way they would /// view and reseed from `entry.history` exactly the way they would
/// from a connect-time `Snapshot`. /// from a connect-time `Snapshot`.
/// ///
/// Payload is the JSON form of `session_store::LogEntry::SessionStart`. /// Payload is the JSON form of `session_store::LogEntry::SegmentStart`.
SessionRotated { SegmentRotated {
entry: serde_json::Value, entry: serde_json::Value,
}, },
/// Current Pod controller status. Broadcast on every controller-level /// Current Pod controller status. Broadcast on every controller-level
@ -400,15 +400,15 @@ pub enum Event {
/// Pod has started compacting the current session. /// Pod has started compacting the current session.
/// ///
/// Fired immediately before a compaction run. Success is signalled by /// Fired immediately before a compaction run. Success is signalled by
/// `CompactDone` (with the new `SessionId`); failure by `CompactFailed`. /// `CompactDone` (with the new `SegmentId`); failure by `CompactFailed`.
/// Broadcast to all clients; not replayed to late subscribers. /// Broadcast to all clients; not replayed to late subscribers.
CompactStart, CompactStart,
/// Compaction completed and the session was rotated. /// Compaction completed and the session was rotated.
/// ///
/// `new_session_id` is the UUID of the freshly created session that /// `new_segment_id` is the UUID of the freshly created session that
/// replaced the old history. /// replaced the old history.
CompactDone { CompactDone {
new_session_id: uuid::Uuid, new_segment_id: uuid::Uuid,
}, },
/// Compaction failed. The session is unchanged. /// Compaction failed. The session is unchanged.
CompactFailed { CompactFailed {
@ -890,18 +890,18 @@ mod tests {
} }
#[test] #[test]
fn event_session_rotated_roundtrip() { fn event_segment_rotated_roundtrip() {
let event = Event::SessionRotated { let event = Event::SegmentRotated {
entry: serde_json::json!({"kind": "session_start", "ts": 1, "history": []}), entry: serde_json::json!({"kind": "segment_start", "ts": 1, "history": []}),
}; };
let json = serde_json::to_string(&event).unwrap(); let json = serde_json::to_string(&event).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["event"], "session_rotated"); assert_eq!(parsed["event"], "segment_rotated");
assert_eq!(parsed["data"]["entry"]["kind"], "session_start"); assert_eq!(parsed["data"]["entry"]["kind"], "segment_start");
let decoded: Event = serde_json::from_str(&json).unwrap(); let decoded: Event = serde_json::from_str(&json).unwrap();
match decoded { match decoded {
Event::SessionRotated { entry } => assert_eq!(entry["kind"], "session_start"), Event::SegmentRotated { entry } => assert_eq!(entry["kind"], "segment_start"),
other => panic!("expected SessionRotated, got {other:?}"), other => panic!("expected SegmentRotated, got {other:?}"),
} }
} }
@ -1060,17 +1060,17 @@ mod tests {
#[test] #[test]
fn event_compact_done_roundtrip() { fn event_compact_done_roundtrip() {
let id = uuid::Uuid::parse_str("0192f0e8-4d84-7d6e-a000-000000000001").unwrap(); let id = uuid::Uuid::parse_str("0192f0e8-4d84-7d6e-a000-000000000001").unwrap();
let event = Event::CompactDone { new_session_id: id }; let event = Event::CompactDone { new_segment_id: id };
let json = serde_json::to_string(&event).unwrap(); let json = serde_json::to_string(&event).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["event"], "compact_done"); assert_eq!(parsed["event"], "compact_done");
assert_eq!( assert_eq!(
parsed["data"]["new_session_id"], parsed["data"]["new_segment_id"],
"0192f0e8-4d84-7d6e-a000-000000000001" "0192f0e8-4d84-7d6e-a000-000000000001"
); );
let decoded: Event = serde_json::from_str(&json).unwrap(); let decoded: Event = serde_json::from_str(&json).unwrap();
match decoded { match decoded {
Event::CompactDone { new_session_id } => assert_eq!(new_session_id, id), Event::CompactDone { new_segment_id } => assert_eq!(new_segment_id, id),
other => panic!("expected CompactDone, got {other:?}"), other => panic!("expected CompactDone, got {other:?}"),
} }
} }

View File

@ -18,7 +18,7 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use session_store::{SessionId, Store, StoreError, save_extension, session_log}; use session_store::{SegmentId, Store, StoreError, save_extension, segment_log};
/// Domain tag used in `LogEntry::Extension` for all metrics records. /// Domain tag used in `LogEntry::Extension` for all metrics records.
pub const DOMAIN: &str = "metrics"; pub const DOMAIN: &str = "metrics";
@ -48,7 +48,7 @@ impl Metric {
pub fn now(name: impl Into<String>) -> Self { pub fn now(name: impl Into<String>) -> Self {
Self { Self {
name: name.into(), name: name.into(),
ts: session_log::now_millis(), ts: segment_log::now_millis(),
dimensions: BTreeMap::new(), dimensions: BTreeMap::new(),
value: None, value: None,
correlation_id: None, correlation_id: None,
@ -77,11 +77,11 @@ impl Metric {
/// (メトリクスのために本体処理を止めるかは呼び出し側の判断)。 /// (メトリクスのために本体処理を止めるかは呼び出し側の判断)。
pub fn record_metric( pub fn record_metric(
store: &impl Store, store: &impl Store,
session_id: SessionId, segment_id: SegmentId,
metric: &Metric, metric: &Metric,
) -> Result<(), StoreError> { ) -> Result<(), StoreError> {
let payload = serde_json::to_value(metric).expect("Metric serialization cannot fail"); let payload = serde_json::to_value(metric).expect("Metric serialization cannot fail");
save_extension(store, session_id, DOMAIN, payload) save_extension(store, segment_id, DOMAIN, payload)
} }
/// `RestoredState.extensions` から metrics domain の payload を順に取り出し、 /// `RestoredState.extensions` から metrics domain の payload を順に取り出し、

View File

@ -2,7 +2,7 @@
//! //!
//! [`TraceEntry`] captures every LLM stream event verbatim for debugging //! [`TraceEntry`] captures every LLM stream event verbatim for debugging
//! and post-hoc analysis. Written to a separate `.trace.jsonl` file, //! 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`. //! Disabled by default. Enable via `SessionConfig::record_event_trace`.

View File

@ -1,12 +1,12 @@
//! Filesystem-backed JSONL store. //! Filesystem-backed JSONL store.
//! //!
//! Layout: //! Layout:
//! - Session log: `{root}/{session_id}.jsonl` //! - Segment log: `{root}/{segment_id}.jsonl`
//! - Event trace: `{root}/{session_id}.trace.jsonl` //! - Event trace: `{root}/{segment_id}.trace.jsonl`
use crate::SessionId; use crate::SegmentId;
use crate::event_trace::TraceEntry; use crate::event_trace::TraceEntry;
use crate::session_log::LogEntry; use crate::segment_log::LogEntry;
use crate::store::{Store, StoreError}; use crate::store::{Store, StoreError};
use std::fs; use std::fs;
use std::io::Write; use std::io::Write;
@ -14,7 +14,7 @@ use std::path::{Path, PathBuf};
/// Filesystem-backed JSONL store. /// 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. /// per line. Writes use append mode for crash safety.
#[derive(Clone)] #[derive(Clone)]
pub struct FsStore { pub struct FsStore {
@ -30,11 +30,11 @@ impl FsStore {
Ok(Self { root }) Ok(Self { root })
} }
fn log_path(&self, id: SessionId) -> PathBuf { fn log_path(&self, id: SegmentId) -> PathBuf {
self.root.join(format!("{id}.jsonl")) 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")) self.root.join(format!("{id}.trace.jsonl"))
} }
@ -65,12 +65,12 @@ impl FsStore {
} }
impl Store for 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)?; let line = serde_json::to_string(entry)?;
self.append_line(&self.log_path(id), &line) 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); let path = self.log_path(id);
if !path.exists() { if !path.exists() {
return Err(StoreError::NotFound(id)); return Err(StoreError::NotFound(id));
@ -79,8 +79,8 @@ impl Store for FsStore {
Self::parse_jsonl(&content) 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(); let mut segments = Vec::new();
for entry in fs::read_dir(&self.root)? { for entry in fs::read_dir(&self.root)? {
let entry = entry?; let entry = entry?;
let path = entry.path(); let path = entry.path();
@ -88,17 +88,17 @@ impl Store for FsStore {
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if name.ends_with(".jsonl") && !name.ends_with(".trace.jsonl") { if name.ends_with(".jsonl") && !name.ends_with(".trace.jsonl") {
let stem = name.trim_end_matches(".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); segments.push(id);
} }
} }
} }
// UUID v7: lexicographic sort = chronological sort, newest first // UUID v7: lexicographic sort = chronological sort, newest first
sessions.sort_by(|a, b| b.cmp(a)); segments.sort_by(|a, b| b.cmp(a));
Ok(sessions) Ok(segments)
} }
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 path = self.log_path(id);
let mut content = String::new(); let mut content = String::new();
for entry in entries { for entry in entries {
@ -109,11 +109,11 @@ impl Store for FsStore {
Ok(()) Ok(())
} }
fn exists(&self, id: SessionId) -> Result<bool, StoreError> { fn exists(&self, id: SegmentId) -> Result<bool, StoreError> {
Ok(self.log_path(id).exists()) 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); let path = self.log_path(id);
if !path.exists() { if !path.exists() {
return Err(StoreError::NotFound(id)); return Err(StoreError::NotFound(id));
@ -122,7 +122,7 @@ impl Store for FsStore {
Ok(content.lines().filter(|l| !l.trim().is_empty()).count()) 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)?; let line = serde_json::to_string(entry)?;
self.append_line(&self.trace_path(id), &line) self.append_line(&self.trace_path(id), &line)
} }

View File

@ -1,4 +1,4 @@
//! Session persistence via append-only JSONL logs. //! Segment persistence via append-only JSONL logs.
//! //!
//! # Architecture //! # Architecture
//! //!
@ -11,15 +11,15 @@
//! functions after state-mutating operations. //! functions after state-mutating operations.
//! //!
//! Debug-mode [`TraceEntry`] records capture raw stream events in a separate //! 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 //! # Quick start
//! //!
//! ```ignore //! ```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 store = FsStore::new("./sessions")?;
//! let session_id = create_session(&store, SessionStartState { //! let segment_id = create_segment(&store, SegmentStartState {
//! system_prompt: None, //! system_prompt: None,
//! config: &config, //! config: &config,
//! history: &[], //! history: &[],
@ -29,8 +29,8 @@
pub mod event_trace; pub mod event_trace;
pub mod fs_store; pub mod fs_store;
pub mod logged_item; pub mod logged_item;
pub mod session; pub mod segment;
pub mod session_log; pub mod segment_log;
pub mod store; pub mod store;
pub mod system_item; pub mod system_item;
@ -39,23 +39,23 @@ pub use fs_store::FsStore;
pub use llm_worker::UsageRecord; pub use llm_worker::UsageRecord;
pub use llm_worker::llm_client::types::{ContentPart, Item, Role}; pub use llm_worker::llm_client::types::{ContentPart, Item, Role};
pub use logged_item::{LoggedContentPart, LoggedItem, LoggedRole, from_logged, to_logged}; pub use logged_item::{LoggedContentPart, LoggedItem, LoggedRole, from_logged, to_logged};
pub use session::{ pub use segment::{
SessionStartState, append_entry, append_system_item, classify_history_item, SegmentStartState, append_entry, append_system_item, classify_history_item,
create_compacted_session, create_session, create_session_with_id, ensure_head_or_fork, fork, 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, 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, save_run_completed, save_run_errored, save_turn_end, save_usage, save_user_input,
}; };
pub use session_log::{ pub use segment_log::{
LogEntry, POD_SCOPE_EXTENSION_DOMAIN, PodScopeSnapshot, RestoredState, SessionOrigin, LogEntry, POD_SCOPE_EXTENSION_DOMAIN, PodScopeSnapshot, RestoredState, SegmentOrigin,
collect_state, collect_state,
}; };
pub use system_item::{SystemItem, render_pod_event}; pub use system_item::{SystemItem, render_pod_event};
pub use store::{Store, StoreError}; pub use store::{Store, StoreError};
/// Session identifier. UUID v7 (time-ordered, lexicographically sortable). /// Segment identifier. UUID v7 (time-ordered, lexicographically sortable).
pub type SessionId = uuid::Uuid; pub type SegmentId = uuid::Uuid;
/// Generate a new session ID. /// Generate a new segment ID.
pub fn new_session_id() -> SessionId { pub fn new_segment_id() -> SegmentId {
uuid::Uuid::now_v7() uuid::Uuid::now_v7()
} }

View File

@ -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 //! The caller (typically Pod) holds the Worker directly and calls these
//! functions after state-mutating operations. //! functions after state-mutating operations.
use crate::SessionId; use crate::SegmentId;
use crate::logged_item::{LoggedItem, to_logged}; 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::store::{Store, StoreError};
use crate::system_item::SystemItem; use crate::system_item::SystemItem;
use llm_worker::WorkerResult; use llm_worker::WorkerResult;
@ -14,108 +14,108 @@ use llm_worker::llm_client::RequestConfig;
use llm_worker::llm_client::types::Item; use llm_worker::llm_client::types::Item;
use protocol::Segment; use protocol::Segment;
/// State snapshot for creating a SessionStart entry. /// State snapshot for creating a SegmentStart entry.
pub struct SessionStartState<'a> { pub struct SegmentStartState<'a> {
pub system_prompt: Option<&'a str>, pub system_prompt: Option<&'a str>,
pub config: &'a RequestConfig, pub config: &'a RequestConfig,
pub history: &'a [Item], pub history: &'a [Item],
} }
/// Create a new session, writing the initial `SessionStart` entry. /// Create a new segment, writing the initial `SegmentStart` entry.
pub fn create_session( pub fn create_segment(
store: &impl Store, store: &impl Store,
state: SessionStartState<'_>, state: SegmentStartState<'_>,
) -> Result<SessionId, StoreError> { ) -> Result<SegmentId, StoreError> {
let session_id = crate::new_session_id(); let segment_id = crate::new_segment_id();
create_session_with_id(store, session_id, state)?; create_segment_with_id(store, segment_id, state)?;
Ok(session_id) 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 /// defer the initial log append (e.g. Pod, which resolves a templated
/// system prompt only at first turn). /// system prompt only at first turn).
pub fn create_session_with_id( pub fn create_segment_with_id(
store: &impl Store, store: &impl Store,
session_id: SessionId, segment_id: SegmentId,
state: SessionStartState<'_>, state: SegmentStartState<'_>,
) -> Result<(), StoreError> { ) -> Result<(), StoreError> {
let entry = LogEntry::SessionStart { let entry = LogEntry::SegmentStart {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
system_prompt: state.system_prompt.map(String::from), system_prompt: state.system_prompt.map(String::from),
config: state.config.clone(), config: state.config.clone(),
history: to_logged(state.history), history: to_logged(state.history),
forked_from: None, forked_from: None,
compacted_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 /// at the turn boundary captured by `source_turn_count` (the most recent
/// completed turn in the source). /// completed turn in the source).
pub fn create_compacted_session( pub fn create_compacted_segment(
store: &impl Store, store: &impl Store,
state: SessionStartState<'_>, state: SegmentStartState<'_>,
source_session_id: SessionId, source_segment_id: SegmentId,
source_turn_count: usize, source_turn_count: usize,
) -> Result<SessionId, StoreError> { ) -> Result<SegmentId, StoreError> {
let session_id = crate::new_session_id(); let segment_id = crate::new_segment_id();
let entry = LogEntry::SessionStart { let entry = LogEntry::SegmentStart {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
system_prompt: state.system_prompt.map(String::from), system_prompt: state.system_prompt.map(String::from),
config: state.config.clone(), config: state.config.clone(),
history: to_logged(state.history), history: to_logged(state.history),
forked_from: None, forked_from: None,
compacted_from: Some(SessionOrigin { compacted_from: Some(SegmentOrigin {
session_id: source_session_id, segment_id: source_segment_id,
at_turn_index: source_turn_count, at_turn_index: source_turn_count,
}), }),
}; };
store.append(session_id, &entry)?; store.append(segment_id, &entry)?;
Ok(session_id) 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 /// Returns the reconstructed state. The caller is responsible for
/// applying it to a Worker. /// applying it to a Worker.
pub fn restore( pub fn restore(
store: &impl Store, store: &impl Store,
session_id: SessionId, segment_id: SegmentId,
) -> Result<crate::session_log::RestoredState, StoreError> { ) -> Result<crate::segment_log::RestoredState, StoreError> {
let entries = store.read_all(session_id)?; let entries = store.read_all(segment_id)?;
Ok(session_log::collect_state(&entries)) Ok(segment_log::collect_state(&entries))
} }
/// Check if the store's entry count still matches the writer's tally. /// 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( pub fn ensure_head_or_fork(
store: &impl Store, store: &impl Store,
session_id: &mut SessionId, segment_id: &mut SegmentId,
entries_written: &mut usize, entries_written: &mut usize,
state: SessionStartState<'_>, state: SegmentStartState<'_>,
) -> Result<(), StoreError> { ) -> 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 { if store_count == *entries_written {
return Ok(()); return Ok(());
} }
let fork_id = crate::new_session_id(); let fork_id = crate::new_segment_id();
let entry = LogEntry::SessionStart { let entry = LogEntry::SegmentStart {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
system_prompt: state.system_prompt.map(String::from), system_prompt: state.system_prompt.map(String::from),
config: state.config.clone(), config: state.config.clone(),
history: to_logged(state.history), history: to_logged(state.history),
forked_from: None, forked_from: None,
compacted_from: None, compacted_from: None,
}; };
store.create_session(fork_id, &[entry])?; store.create_segment(fork_id, &[entry])?;
*session_id = fork_id; *segment_id = fork_id;
*entries_written = 1; *entries_written = 1;
Ok(()) Ok(())
} }
@ -128,14 +128,14 @@ pub fn ensure_head_or_fork(
/// [`Segment::flatten_to_text`]. /// [`Segment::flatten_to_text`].
pub fn save_user_input( pub fn save_user_input(
store: &impl Store, store: &impl Store,
session_id: SessionId, segment_id: SegmentId,
segments: Vec<Segment>, segments: Vec<Segment>,
) -> Result<(), StoreError> { ) -> Result<(), StoreError> {
append_entry( append_entry(
store, store,
session_id, segment_id,
LogEntry::UserInput { LogEntry::UserInput {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
segments, segments,
}, },
) )
@ -151,21 +151,21 @@ pub fn save_user_input(
/// `UserInput` entry. /// `UserInput` entry.
pub fn save_delta( pub fn save_delta(
store: &impl Store, store: &impl Store,
session_id: SessionId, segment_id: SegmentId,
new_items: &[Item], new_items: &[Item],
) -> Result<(), StoreError> { ) -> Result<(), StoreError> {
if new_items.is_empty() { if new_items.is_empty() {
return Ok(()); return Ok(());
} }
let ts = session_log::now_millis(); let ts = segment_log::now_millis();
for item in new_items { for item in new_items {
if item.is_user_message() { if item.is_user_message() {
// Already persisted by save_user_input at submit time. // Already persisted by save_user_input at submit time.
continue; continue;
} }
let entry = classify_history_item(item, ts); let entry = classify_history_item(item, ts);
append_entry(store, session_id, entry)?; append_entry(store, segment_id, entry)?;
} }
Ok(()) Ok(())
} }
@ -199,14 +199,14 @@ pub fn classify_history_item(item: &Item, ts: u64) -> LogEntry {
/// commit shape used for assistant / tool result entries. /// commit shape used for assistant / tool result entries.
pub fn append_system_item( pub fn append_system_item(
store: &impl Store, store: &impl Store,
session_id: SessionId, segment_id: SegmentId,
item: SystemItem, item: SystemItem,
) -> Result<(), StoreError> { ) -> Result<(), StoreError> {
append_entry( append_entry(
store, store,
session_id, segment_id,
LogEntry::SystemItem { LogEntry::SystemItem {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
item, item,
}, },
) )
@ -215,14 +215,14 @@ pub fn append_system_item(
/// Log a TurnEnd entry. /// Log a TurnEnd entry.
pub fn save_turn_end( pub fn save_turn_end(
store: &impl Store, store: &impl Store,
session_id: SessionId, segment_id: SegmentId,
turn_count: usize, turn_count: usize,
) -> Result<(), StoreError> { ) -> Result<(), StoreError> {
append_entry( append_entry(
store, store,
session_id, segment_id,
LogEntry::TurnEnd { LogEntry::TurnEnd {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
turn_count, turn_count,
}, },
) )
@ -231,15 +231,15 @@ pub fn save_turn_end(
/// Log a `RunCompleted` entry — `run()` / `resume()` returned `Ok(WorkerResult)`. /// Log a `RunCompleted` entry — `run()` / `resume()` returned `Ok(WorkerResult)`.
pub fn save_run_completed( pub fn save_run_completed(
store: &impl Store, store: &impl Store,
session_id: SessionId, segment_id: SegmentId,
result: WorkerResult, result: WorkerResult,
interrupted: bool, interrupted: bool,
) -> Result<(), StoreError> { ) -> Result<(), StoreError> {
append_entry( append_entry(
store, store,
session_id, segment_id,
LogEntry::RunCompleted { LogEntry::RunCompleted {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
interrupted, interrupted,
result, result,
}, },
@ -252,15 +252,15 @@ pub fn save_run_completed(
/// `to_string()` rendering as `message`. /// `to_string()` rendering as `message`.
pub fn save_run_errored( pub fn save_run_errored(
store: &impl Store, store: &impl Store,
session_id: SessionId, segment_id: SegmentId,
message: String, message: String,
interrupted: bool, interrupted: bool,
) -> Result<(), StoreError> { ) -> Result<(), StoreError> {
append_entry( append_entry(
store, store,
session_id, segment_id,
LogEntry::RunErrored { LogEntry::RunErrored {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
interrupted, interrupted,
message, message,
}, },
@ -275,7 +275,7 @@ pub fn save_run_errored(
/// 済ませた値を渡す。 /// 済ませた値を渡す。
pub fn save_usage( pub fn save_usage(
store: &impl Store, store: &impl Store,
session_id: SessionId, segment_id: SegmentId,
history_len: usize, history_len: usize,
input_total_tokens: u64, input_total_tokens: u64,
cache_read_tokens: u64, cache_read_tokens: u64,
@ -284,9 +284,9 @@ pub fn save_usage(
) -> Result<(), StoreError> { ) -> Result<(), StoreError> {
append_entry( append_entry(
store, store,
session_id, segment_id,
LogEntry::LlmUsage { LogEntry::LlmUsage {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
history_len, history_len,
input_total_tokens, input_total_tokens,
cache_read_tokens, cache_read_tokens,
@ -303,15 +303,15 @@ pub fn save_usage(
/// Use `RestoredState.extensions` to read entries back at restore time. /// Use `RestoredState.extensions` to read entries back at restore time.
pub fn save_extension( pub fn save_extension(
store: &impl Store, store: &impl Store,
session_id: SessionId, segment_id: SegmentId,
domain: impl Into<String>, domain: impl Into<String>,
payload: serde_json::Value, payload: serde_json::Value,
) -> Result<(), StoreError> { ) -> Result<(), StoreError> {
append_entry( append_entry(
store, store,
session_id, segment_id,
LogEntry::Extension { LogEntry::Extension {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
domain: domain.into(), domain: domain.into(),
payload, payload,
}, },
@ -321,14 +321,14 @@ pub fn save_extension(
/// Log the Pod's latest runtime scope snapshot. /// Log the Pod's latest runtime scope snapshot.
pub fn save_pod_scope( pub fn save_pod_scope(
store: &impl Store, store: &impl Store,
session_id: SessionId, segment_id: SegmentId,
snapshot: &PodScopeSnapshot, snapshot: &PodScopeSnapshot,
) -> Result<(), StoreError> { ) -> Result<(), StoreError> {
let payload = serde_json::to_value(snapshot)?; let payload = serde_json::to_value(snapshot)?;
save_extension( save_extension(
store, store,
session_id, segment_id,
session_log::POD_SCOPE_EXTENSION_DOMAIN, segment_log::POD_SCOPE_EXTENSION_DOMAIN,
payload, payload,
) )
} }
@ -336,35 +336,35 @@ pub fn save_pod_scope(
/// Log a `ConfigChanged` entry. /// Log a `ConfigChanged` entry.
pub fn save_config_changed( pub fn save_config_changed(
store: &impl Store, store: &impl Store,
session_id: SessionId, segment_id: SegmentId,
config: &RequestConfig, config: &RequestConfig,
) -> Result<(), StoreError> { ) -> Result<(), StoreError> {
append_entry( append_entry(
store, store,
session_id, segment_id,
LogEntry::ConfigChanged { LogEntry::ConfigChanged {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
config: config.clone(), config: config.clone(),
}, },
) )
} }
/// Fork the current state into a new session. /// Fork the current state into a new segment.
pub fn fork(store: &impl Store, state: SessionStartState<'_>) -> Result<SessionId, StoreError> { pub fn fork(store: &impl Store, state: SegmentStartState<'_>) -> Result<SegmentId, StoreError> {
let fork_id = crate::new_session_id(); let fork_id = crate::new_segment_id();
let entry = LogEntry::SessionStart { let entry = LogEntry::SegmentStart {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
system_prompt: state.system_prompt.map(String::from), system_prompt: state.system_prompt.map(String::from),
config: state.config.clone(), config: state.config.clone(),
history: to_logged(state.history), history: to_logged(state.history),
forked_from: None, forked_from: None,
compacted_from: None, compacted_from: None,
}; };
store.create_session(fork_id, &[entry])?; store.create_segment(fork_id, &[entry])?;
Ok(fork_id) 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 /// `at_turn_index` is the `turn_count` of the most recent completed
/// `TurnEnd` in the source segment that the fork should branch from. /// `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. /// after it are not carried into the new segment.
pub fn fork_at( pub fn fork_at(
store: &impl Store, store: &impl Store,
source_id: SessionId, source_id: SegmentId,
at_turn_index: usize, at_turn_index: usize,
) -> Result<SessionId, StoreError> { ) -> Result<SegmentId, StoreError> {
let entries = store.read_all(source_id)?; let entries = store.read_all(source_id)?;
let cut = if at_turn_index == 0 { 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. // segment), before any turn completes.
entries entries
.iter() .iter()
.position(|e| !matches!(e, LogEntry::SessionStart { .. })) .position(|e| !matches!(e, LogEntry::SegmentStart { .. }))
.unwrap_or(entries.len()) .unwrap_or(entries.len())
} else { } else {
entries entries
@ -390,21 +390,21 @@ pub fn fork_at(
.map(|i| i + 1) .map(|i| i + 1)
.unwrap_or(entries.len()) .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 fork_id = crate::new_segment_id();
let entry = LogEntry::SessionStart { let entry = LogEntry::SegmentStart {
ts: session_log::now_millis(), ts: segment_log::now_millis(),
system_prompt: state.system_prompt, system_prompt: state.system_prompt,
config: state.config, config: state.config,
history: to_logged(&state.history), history: to_logged(&state.history),
forked_from: Some(SessionOrigin { forked_from: Some(SegmentOrigin {
session_id: source_id, segment_id: source_id,
at_turn_index, at_turn_index,
}), }),
compacted_from: None, compacted_from: None,
}; };
store.create_session(fork_id, &[entry])?; store.create_segment(fork_id, &[entry])?;
Ok(fork_id) Ok(fork_id)
} }
@ -415,8 +415,8 @@ pub fn fork_at(
/// it needs the same value for an in-memory mirror + broadcast). /// it needs the same value for an in-memory mirror + broadcast).
pub fn append_entry( pub fn append_entry(
store: &impl Store, store: &impl Store,
session_id: SessionId, segment_id: SegmentId,
entry: LogEntry, entry: LogEntry,
) -> Result<(), StoreError> { ) -> Result<(), StoreError> {
store.append(session_id, &entry) store.append(segment_id, &entry)
} }

View File

@ -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, //! Each [`LogEntry`] represents a single state transition within one
//! serialized as one line in a `.jsonl` file. Reading all entries and //! segment, serialized as one line in a `.jsonl` file. Reading all
//! collecting them via [`collect_state`] reconstructs the full [`Worker`] state. //! 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 //! The on-disk format is one `LogEntry` per line — entries are positionally
//! ordered. Fork lineage references between segments use turn-number indices //! 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::llm_client::types::{Item, RequestConfig};
use llm_worker::{UsageRecord, WorkerResult}; use llm_worker::{UsageRecord, WorkerResult};
@ -16,10 +17,10 @@ use serde::{Deserialize, Serialize};
use crate::logged_item::LoggedItem; use crate::logged_item::LoggedItem;
use crate::system_item::SystemItem; 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`: /// 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) /// - `Invoke` — IDLE → active marker (start of a new self-driving cycle)
/// - `UserInput` / `AssistantItems` / `ToolResults` / `HookInjectedItems` — history appends /// - `UserInput` / `AssistantItems` / `ToolResults` / `HookInjectedItems` — history appends
/// - `TurnEnd` — AgentTurn boundary marker; carries the post-increment /// - `TurnEnd` — AgentTurn boundary marker; carries the post-increment
@ -32,19 +33,19 @@ use crate::system_item::SystemItem;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")] #[serde(tag = "kind", rename_all = "snake_case")]
pub enum LogEntry { pub enum LogEntry {
/// Session start. Always the first entry in a log. /// Segment start. Always the first entry in a segment log.
/// For forked sessions, `history` contains the seed state from the parent. /// For forked segments, `history` contains the seed state from the parent.
SessionStart { SegmentStart {
ts: u64, ts: u64,
system_prompt: Option<String>, system_prompt: Option<String>,
config: RequestConfig, config: RequestConfig,
history: Vec<LoggedItem>, 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")] #[serde(default, skip_serializing_if = "Option::is_none")]
forked_from: Option<SessionOrigin>, forked_from: Option<SegmentOrigin>,
/// Origin: compacted from another session at a specific turn boundary. /// Origin: compacted from another segment at a specific turn boundary.
#[serde(default, skip_serializing_if = "Option::is_none")] #[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 /// 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 /// User input accepted at submit time. Carries the original typed
/// `Vec<Segment>` so clients can re-render typed atoms (paste chips, /// `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 /// Replay flattens these into a `Item::user_message` for the worker
/// history; the worker layer never sees segments directly. /// history; the worker layer never sees segments directly.
UserInput { ts: u64, segments: Vec<Segment> }, UserInput { ts: u64, segments: Vec<Segment> },
@ -87,7 +88,7 @@ pub enum LogEntry {
/// dispatch on `kind` for typed rendering. /// dispatch on `kind` for typed rendering.
SystemItem { ts: u64, item: SystemItem }, 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 /// open. New writes always use the singular `AssistantItem`. Items
/// are flattened on replay. /// are flattened on replay.
AssistantItems { ts: u64, items: Vec<LoggedItem> }, 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 /// `at_turn_index` is the `turn_count` value of the most recent
/// `TurnEnd` entry preceding the split point in the source segment. /// `TurnEnd` entry preceding the split point in the source segment.
/// A value of `0` means the split happened before any turn completed /// 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)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SessionOrigin { pub struct SegmentOrigin {
pub session_id: crate::SessionId, pub segment_id: crate::SegmentId,
pub at_turn_index: usize, pub at_turn_index: usize,
} }
@ -194,7 +195,7 @@ pub struct RestoredState {
pub history: Vec<Item>, pub history: Vec<Item>,
pub turn_count: usize, pub turn_count: usize,
pub last_run_interrupted: bool, 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 /// Writers track their own append count via the same counter so
/// `ensure_head_or_fork` can compare it with the on-disk count. /// `ensure_head_or_fork` can compare it with the on-disk count.
pub entries_count: usize, pub entries_count: usize,
@ -206,14 +207,14 @@ pub struct RestoredState {
/// session-store は domain を不透明扱いし、各ドメインが自前で fold する。 /// session-store は domain を不透明扱いし、各ドメインが自前で fold する。
pub extensions: Vec<(String, serde_json::Value)>, pub extensions: Vec<(String, serde_json::Value)>,
/// Latest runtime scope snapshot persisted by the Pod. `None` means /// 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>, pub pod_scope: Option<PodScopeSnapshot>,
/// User submissions in original typed form, in submit order. /// User submissions in original typed form, in submit order.
/// One entry per `LogEntry::UserInput`; the K-th entry corresponds to /// One entry per `LogEntry::UserInput`; the K-th entry corresponds to
/// the K-th `Item::user_message` derived during replay (modulo /// 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 /// 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>>, pub user_segments: Vec<Vec<Segment>>,
} }
@ -236,7 +237,7 @@ pub fn collect_state(entries: &[LogEntry]) -> RestoredState {
state.entries_count += 1; state.entries_count += 1;
match entry { match entry {
LogEntry::SessionStart { LogEntry::SegmentStart {
system_prompt, system_prompt,
config, config,
history, history,
@ -316,7 +317,7 @@ pub fn collect_state(entries: &[LogEntry]) -> RestoredState {
Err(err) => { Err(err) => {
tracing::warn!( tracing::warn!(
error = %err, 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] #[test]
fn replay_session_start_sets_initial_state() { fn replay_segment_start_sets_initial_state() {
let state = collect_state(&[LogEntry::SessionStart { let state = collect_state(&[LogEntry::SegmentStart {
ts: 1000, ts: 1000,
system_prompt: Some("You are helpful.".into()), system_prompt: Some("You are helpful.".into()),
config: RequestConfig::default().with_max_tokens(1024), config: RequestConfig::default().with_max_tokens(1024),
@ -368,7 +369,7 @@ mod tests {
#[test] #[test]
fn replay_full_turn() { fn replay_full_turn() {
let state = collect_state(&[ let state = collect_state(&[
LogEntry::SessionStart { LogEntry::SegmentStart {
ts: 1000, ts: 1000,
system_prompt: None, system_prompt: None,
config: RequestConfig::default(), config: RequestConfig::default(),
@ -402,7 +403,7 @@ mod tests {
#[test] #[test]
fn replay_with_tool_calls() { fn replay_with_tool_calls() {
let state = collect_state(&[ let state = collect_state(&[
LogEntry::SessionStart { LogEntry::SegmentStart {
ts: 1000, ts: 1000,
system_prompt: None, system_prompt: None,
config: RequestConfig::default(), config: RequestConfig::default(),
@ -439,7 +440,7 @@ mod tests {
#[test] #[test]
fn replay_config_changed() { fn replay_config_changed() {
let state = collect_state(&[ let state = collect_state(&[
LogEntry::SessionStart { LogEntry::SegmentStart {
ts: 1000, ts: 1000,
system_prompt: None, system_prompt: None,
config: RequestConfig::default(), config: RequestConfig::default(),
@ -458,7 +459,7 @@ mod tests {
#[test] #[test]
fn replay_llm_usage_appends_to_usage_history() { fn replay_llm_usage_appends_to_usage_history() {
let state = collect_state(&[ let state = collect_state(&[
LogEntry::SessionStart { LogEntry::SegmentStart {
ts: 1000, ts: 1000,
system_prompt: None, system_prompt: None,
config: RequestConfig::default(), config: RequestConfig::default(),
@ -504,7 +505,7 @@ mod tests {
#[test] #[test]
fn replay_without_llm_usage_keeps_usage_history_empty() { fn replay_without_llm_usage_keeps_usage_history_empty() {
let state = collect_state(&[ let state = collect_state(&[
LogEntry::SessionStart { LogEntry::SegmentStart {
ts: 1000, ts: 1000,
system_prompt: None, system_prompt: None,
config: RequestConfig::default(), config: RequestConfig::default(),
@ -575,7 +576,7 @@ mod tests {
#[test] #[test]
fn replay_invoke_marker_does_not_mutate_state() { fn replay_invoke_marker_does_not_mutate_state() {
let state = collect_state(&[ let state = collect_state(&[
LogEntry::SessionStart { LogEntry::SegmentStart {
ts: 0, ts: 0,
system_prompt: None, system_prompt: None,
config: RequestConfig::default(), config: RequestConfig::default(),
@ -607,7 +608,7 @@ mod tests {
#[test] #[test]
fn replay_extension_collects_domain_payload_pairs() { fn replay_extension_collects_domain_payload_pairs() {
let state = collect_state(&[ let state = collect_state(&[
LogEntry::SessionStart { LogEntry::SegmentStart {
ts: 1000, ts: 1000,
system_prompt: None, system_prompt: None,
config: RequestConfig::default(), config: RequestConfig::default(),
@ -690,7 +691,7 @@ mod tests {
let json = serde_json::to_string(&entry).unwrap(); let json = serde_json::to_string(&entry).unwrap();
let parsed: LogEntry = serde_json::from_str(&json).unwrap(); let parsed: LogEntry = serde_json::from_str(&json).unwrap();
let state = collect_state(&[ let state = collect_state(&[
LogEntry::SessionStart { LogEntry::SegmentStart {
ts: 1, ts: 1,
system_prompt: None, system_prompt: None,
config: RequestConfig::default(), config: RequestConfig::default(),

View File

@ -1,18 +1,18 @@
//! Persistence backend abstraction. //! 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.). //! 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 //! `< 1 KiB` line on local fs and completes well below a millisecond. Going
//! through `tokio::fs` would force every caller — including `Worker`'s sync //! through `tokio::fs` would force every caller — including `Worker`'s sync
//! `on_history_append` callback — to bridge sync → async via a channel + //! `on_history_append` callback — to bridge sync → async via a channel +
//! drain task. Keeping the store sync lets the worker callback, Pod commit //! drain task. Keeping the store sync lets the worker callback, Pod commit
//! paths, and `PodInterceptor` all share one direct `append_entry` call. //! paths, and `PodInterceptor` all share one direct `append_entry` call.
use crate::SessionId; use crate::SegmentId;
use crate::event_trace::TraceEntry; use crate::event_trace::TraceEntry;
use crate::session_log::LogEntry; use crate::segment_log::LogEntry;
/// Errors from the persistence store. /// Errors from the persistence store.
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
@ -23,43 +23,43 @@ pub enum StoreError {
#[error("serialization error: {0}")] #[error("serialization error: {0}")]
Serde(#[from] serde_json::Error), Serde(#[from] serde_json::Error),
#[error("session not found: {0}")] #[error("segment not found: {0}")]
NotFound(SessionId), NotFound(SegmentId),
#[error("log corrupted at line {line}: {message}")] #[error("log corrupted at line {line}: {message}")]
Corrupt { line: usize, message: String }, 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 /// All methods take `&self` — implementations should use interior mutability
/// (e.g., append-mode file handles) when needed. /// (e.g., append-mode file handles) when needed.
pub trait Store: Send + Sync { 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 /// One line per call. The kernel orders concurrent `O_APPEND` writes
/// for lines < `PIPE_BUF`, so user-space serialization is unnecessary. /// 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. /// Read all log entries for a segment, in order.
fn read_all(&self, id: SessionId) -> Result<Vec<LogEntry>, StoreError>; fn read_all(&self, id: SegmentId) -> Result<Vec<LogEntry>, StoreError>;
/// List all session IDs, most recent first. /// List all segment IDs, most recent first.
fn list_sessions(&self) -> Result<Vec<SessionId>, StoreError>; fn list_segments(&self) -> Result<Vec<SegmentId>, StoreError>;
/// Create a new session with initial entries. /// Create a new segment with initial entries.
fn create_session(&self, id: SessionId, entries: &[LogEntry]) -> Result<(), StoreError>; fn create_segment(&self, id: SegmentId, entries: &[LogEntry]) -> Result<(), StoreError>;
/// Check if a session exists. /// Check if a segment exists.
fn exists(&self, id: SessionId) -> Result<bool, StoreError>; 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: /// Used by `ensure_head_or_fork` to detect concurrent writers:
/// if the on-disk count exceeds the writer's own append tally, /// if the on-disk count exceeds the writer's own append tally,
/// another process has extended the log. /// 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. /// 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>;
} }

View File

@ -29,7 +29,7 @@ use serde::{Deserialize, Serialize};
/// path / knowledge slug / workflow slug / etc.), plus a pre-rendered /// path / knowledge slug / workflow slug / etc.), plus a pre-rendered
/// `body` (where applicable) that is the exact `role:system` text the /// `body` (where applicable) that is the exact `role:system` text the
/// LLM actually saw at commit time. `body` is denormalised so that /// 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 /// what was on the wire — even when prompt overrides (e.g. custom
/// `notify_wrapper` template) re-shape the live rendering on a later /// `notify_wrapper` template) re-shape the live rendering on a later
/// resume. /// resume.

View File

@ -1,15 +1,15 @@
use llm_worker::WorkerResult; use llm_worker::WorkerResult;
use llm_worker::llm_client::types::{Item, RequestConfig}; 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] #[test]
fn round_trip_write_and_read() { fn round_trip_write_and_read() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let store = FsStore::new(dir.path()).unwrap(); let store = FsStore::new(dir.path()).unwrap();
let id = new_session_id(); let id = new_segment_id();
let entries = vec![ let entries = vec![
LogEntry::SessionStart { LogEntry::SegmentStart {
ts: 1000, ts: 1000,
system_prompt: Some("You are helpful.".into()), system_prompt: Some("You are helpful.".into()),
config: RequestConfig::default().with_max_tokens(1024), config: RequestConfig::default().with_max_tokens(1024),
@ -56,9 +56,9 @@ fn round_trip_write_and_read() {
fn create_session_writes_all_entries() { fn create_session_writes_all_entries() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let store = FsStore::new(dir.path()).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, ts: 1000,
system_prompt: None, system_prompt: None,
config: RequestConfig::default(), config: RequestConfig::default(),
@ -70,7 +70,7 @@ fn create_session_writes_all_entries() {
compacted_from: None, compacted_from: None,
}]; }];
store.create_session(id, &entries).unwrap(); store.create_segment(id, &entries).unwrap();
let read_back = store.read_all(id).unwrap(); let read_back = store.read_all(id).unwrap();
assert_eq!(read_back.len(), 1); assert_eq!(read_back.len(), 1);
@ -83,12 +83,12 @@ fn list_sessions_returns_newest_first() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let store = FsStore::new(dir.path()).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 // Small delay to ensure different UUID v7 timestamps
std::thread::sleep(std::time::Duration::from_millis(2)); 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, ts: 1000,
system_prompt: None, system_prompt: None,
config: RequestConfig::default(), config: RequestConfig::default(),
@ -100,7 +100,7 @@ fn list_sessions_returns_newest_first() {
store.append(id1, &entry).unwrap(); store.append(id1, &entry).unwrap();
store.append(id2, &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.len(), 2);
assert_eq!(sessions[0], id2); // newest first assert_eq!(sessions[0], id2); // newest first
assert_eq!(sessions[1], id1); assert_eq!(sessions[1], id1);
@ -110,14 +110,14 @@ fn list_sessions_returns_newest_first() {
fn exists_returns_correct_state() { fn exists_returns_correct_state() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let store = FsStore::new(dir.path()).unwrap(); let store = FsStore::new(dir.path()).unwrap();
let id = new_session_id(); let id = new_segment_id();
assert!(!store.exists(id).unwrap()); assert!(!store.exists(id).unwrap());
store store
.append( .append(
id, id,
&LogEntry::SessionStart { &LogEntry::SegmentStart {
ts: 1000, ts: 1000,
system_prompt: None, system_prompt: None,
config: RequestConfig::default(), config: RequestConfig::default(),
@ -135,7 +135,7 @@ fn exists_returns_correct_state() {
fn not_found_error_for_missing_session() { fn not_found_error_for_missing_session() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let store = FsStore::new(dir.path()).unwrap(); let store = FsStore::new(dir.path()).unwrap();
let id = new_session_id(); let id = new_segment_id();
let result = store.read_all(id); let result = store.read_all(id);
assert!(result.is_err()); assert!(result.is_err());
@ -145,12 +145,12 @@ fn not_found_error_for_missing_session() {
fn trace_entries_in_separate_file() { fn trace_entries_in_separate_file() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let store = FsStore::new(dir.path()).unwrap(); let store = FsStore::new(dir.path()).unwrap();
let id = new_session_id(); let id = new_segment_id();
store store
.append( .append(
id, id,
&LogEntry::SessionStart { &LogEntry::SegmentStart {
ts: 1000, ts: 1000,
system_prompt: None, system_prompt: None,
config: RequestConfig::default(), config: RequestConfig::default(),
@ -183,10 +183,10 @@ fn trace_entries_in_separate_file() {
fn read_entry_count_matches_append_tally() { fn read_entry_count_matches_append_tally() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let store = FsStore::new(dir.path()).unwrap(); let store = FsStore::new(dir.path()).unwrap();
let id = new_session_id(); let id = new_segment_id();
let entries = [ let entries = [
LogEntry::SessionStart { LogEntry::SegmentStart {
ts: 1000, ts: 1000,
system_prompt: None, system_prompt: None,
config: RequestConfig::default(), config: RequestConfig::default(),

View File

@ -9,7 +9,7 @@ use llm_worker::interceptor::{Interceptor, TurnEndAction};
use llm_worker::llm_client::event::{Event, ResponseStatus, StatusEvent}; use llm_worker::llm_client::event::{Event, ResponseStatus, StatusEvent};
use llm_worker::llm_client::types::{Item, RequestConfig}; use llm_worker::llm_client::types::{Item, RequestConfig};
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput}; 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 // Helpers
@ -95,13 +95,13 @@ fn make_store() -> (tempfile::TempDir, FsStore) {
async fn run_and_persist( async fn run_and_persist(
worker: Worker<MockLlmClient>, worker: Worker<MockLlmClient>,
store: &FsStore, store: &FsStore,
session_id: session_store::SessionId, segment_id: session_store::SegmentId,
input: &str, input: &str,
) -> (Worker<MockLlmClient>, llm_worker::WorkerResult) { ) -> (Worker<MockLlmClient>, llm_worker::WorkerResult) {
// Mirror Pod's run-entry contract: log the user input as segments // Mirror Pod's run-entry contract: log the user input as segments
// before the worker pushes its flattened user_message; save_delta // before the worker pushes its flattened user_message; save_delta
// skips the resulting user_message item to avoid double-write. // 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(); .unwrap();
let history_before = worker.history().len(); let history_before = worker.history().len();
@ -111,14 +111,14 @@ async fn run_and_persist(
let worker = locked.unlock(); let worker = locked.unlock();
let new_items = &worker.history()[history_before..]; let new_items = &worker.history()[history_before..];
session_store::save_delta(store, session_id, new_items).unwrap(); session_store::save_delta(store, segment_id, new_items).unwrap();
session_store::save_turn_end(store, session_id, worker.turn_count()).unwrap(); session_store::save_turn_end(store, segment_id, worker.turn_count()).unwrap();
match &result { match &result {
Ok(r) => { Ok(r) => {
session_store::save_run_completed( session_store::save_run_completed(
store, store,
session_id, segment_id,
r.clone(), r.clone(),
worker.last_run_interrupted(), worker.last_run_interrupted(),
) )
@ -127,7 +127,7 @@ async fn run_and_persist(
Err(e) => { Err(e) => {
session_store::save_run_errored( session_store::save_run_errored(
store, store,
session_id, segment_id,
e.to_string(), e.to_string(),
worker.last_run_interrupted(), worker.last_run_interrupted(),
) )
@ -149,9 +149,9 @@ async fn session_run_logs_entries() {
let client = MockLlmClient::new(simple_text_events()); let client = MockLlmClient::new(simple_text_events());
let worker = Worker::new(client); let worker = Worker::new(client);
let sid = session_store::create_session( let sid = session_store::create_segment(
&store, &store,
SessionStartState { SegmentStartState {
system_prompt: worker.get_system_prompt(), system_prompt: worker.get_system_prompt(),
config: worker.request_config(), config: worker.request_config(),
history: worker.history(), history: worker.history(),
@ -164,15 +164,15 @@ async fn session_run_logs_entries() {
let entries = store.read_all(sid).unwrap(); let entries = store.read_all(sid).unwrap();
// SessionStart, UserInput, AssistantItems, TurnEnd, RunCompleted (at minimum) // SegmentStart, UserInput, AssistantItems, TurnEnd, RunCompleted (at minimum)
assert!( assert!(
entries.len() >= 4, entries.len() >= 4,
"expected at least 4 entries, got {}", "expected at least 4 entries, got {}",
entries.len() entries.len()
); );
// First entry is SessionStart // First entry is SegmentStart
assert!(matches!(&entries[0], LogEntry::SessionStart { .. })); assert!(matches!(&entries[0], LogEntry::SegmentStart { .. }));
// Has a RunCompleted with Finished // Has a RunCompleted with Finished
let has_finished = entries.iter().any(|e| { let has_finished = entries.iter().any(|e| {
@ -194,9 +194,9 @@ async fn session_restore_round_trip() {
let mut worker = Worker::new(client); let mut worker = Worker::new(client);
worker.set_system_prompt("You are helpful."); worker.set_system_prompt("You are helpful.");
let sid = session_store::create_session( let sid = session_store::create_segment(
&store, &store,
SessionStartState { SegmentStartState {
system_prompt: worker.get_system_prompt(), system_prompt: worker.get_system_prompt(),
config: worker.request_config(), config: worker.request_config(),
history: worker.history(), history: worker.history(),
@ -225,9 +225,9 @@ async fn session_run_with_tool_call() {
let mut worker = Worker::new(client); let mut worker = Worker::new(client);
worker.register_tool(weather_tool_definition()); worker.register_tool(weather_tool_definition());
let sid = session_store::create_session( let sid = session_store::create_segment(
&store, &store,
SessionStartState { SegmentStartState {
system_prompt: worker.get_system_prompt(), system_prompt: worker.get_system_prompt(),
config: worker.request_config(), config: worker.request_config(),
history: worker.history(), history: worker.history(),
@ -260,9 +260,9 @@ async fn session_resume_after_pause() {
worker.register_tool(weather_tool_definition()); worker.register_tool(weather_tool_definition());
worker.set_interceptor(PausePolicy); worker.set_interceptor(PausePolicy);
let sid = session_store::create_session( let sid = session_store::create_segment(
&store, &store,
SessionStartState { SegmentStartState {
system_prompt: worker.get_system_prompt(), system_prompt: worker.get_system_prompt(),
config: worker.request_config(), config: worker.request_config(),
history: worker.history(), history: worker.history(),
@ -298,9 +298,9 @@ async fn session_fork_preserves_state() {
let mut worker = Worker::new(client); let mut worker = Worker::new(client);
worker.set_system_prompt("System prompt"); worker.set_system_prompt("System prompt");
let sid = session_store::create_session( let sid = session_store::create_segment(
&store, &store,
SessionStartState { SegmentStartState {
system_prompt: worker.get_system_prompt(), system_prompt: worker.get_system_prompt(),
config: worker.request_config(), config: worker.request_config(),
history: worker.history(), history: worker.history(),
@ -313,7 +313,7 @@ async fn session_fork_preserves_state() {
let original_history_len = worker.history().len(); let original_history_len = worker.history().len();
let fork_id = session_store::fork( let fork_id = session_store::fork(
&store, &store,
SessionStartState { SegmentStartState {
system_prompt: worker.get_system_prompt(), system_prompt: worker.get_system_prompt(),
config: worker.request_config(), config: worker.request_config(),
history: worker.history(), history: worker.history(),
@ -321,10 +321,10 @@ async fn session_fork_preserves_state() {
) )
.unwrap(); .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(); let fork_entries = store.read_all(fork_id).unwrap();
assert_eq!(fork_entries.len(), 1); 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); let fork_state = collect_state(&fork_entries);
assert_eq!(fork_state.history.len(), original_history_len); 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 client = MockLlmClient::new(simple_text_events());
let worker = Worker::new(client); let worker = Worker::new(client);
let sid = session_store::create_session( let sid = session_store::create_segment(
&store, &store,
SessionStartState { SegmentStartState {
system_prompt: worker.get_system_prompt(), system_prompt: worker.get_system_prompt(),
config: worker.request_config(), config: worker.request_config(),
history: worker.history(), 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_id = session_store::fork_at(&store, sid, worker.turn_count()).unwrap();
let fork_entries = store.read_all(fork_id).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); let fork_state = collect_state(&fork_entries);
// History at fork point should match history right after the TurnEnd in // 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 client = MockLlmClient::new(vec![]);
let mut worker = Worker::new(client); let mut worker = Worker::new(client);
let sid = session_store::create_session( let sid = session_store::create_segment(
&store, &store,
SessionStartState { SegmentStartState {
system_prompt: worker.get_system_prompt(), system_prompt: worker.get_system_prompt(),
config: worker.request_config(), config: worker.request_config(),
history: worker.history(), history: worker.history(),
@ -408,17 +408,17 @@ async fn session_auto_forks_on_conflict() {
let client_a = MockLlmClient::new(simple_text_events()); let client_a = MockLlmClient::new(simple_text_events());
let worker_a = Worker::new(client_a); let worker_a = Worker::new(client_a);
let original_sid = session_store::create_session( let original_sid = session_store::create_segment(
&store, &store,
SessionStartState { SegmentStartState {
system_prompt: worker_a.get_system_prompt(), system_prompt: worker_a.get_system_prompt(),
config: worker_a.request_config(), config: worker_a.request_config(),
history: worker_a.history(), history: worker_a.history(),
}, },
) )
.unwrap(); .unwrap();
let mut session_id = original_sid; let mut segment_id = original_sid;
// Writer tracked: just the SessionStart we wrote. // Writer tracked: just the SegmentStart we wrote.
let mut entries_written: usize = 1; let mut entries_written: usize = 1;
// Simulate another Pod writing to the same session behind our back. // 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. // Now the on-disk count exceeds our tally — ensure_head_or_fork should auto-fork.
session_store::ensure_head_or_fork( session_store::ensure_head_or_fork(
&store, &store,
&mut session_id, &mut segment_id,
&mut entries_written, &mut entries_written,
SessionStartState { SegmentStartState {
system_prompt: worker_a.get_system_prompt(), system_prompt: worker_a.get_system_prompt(),
config: worker_a.request_config(), config: worker_a.request_config(),
history: worker_a.history(), history: worker_a.history(),
@ -441,11 +441,11 @@ async fn session_auto_forks_on_conflict() {
) )
.unwrap(); .unwrap();
// session_id should now be different // segment_id should now be different
assert_ne!(session_id, original_sid); assert_ne!(segment_id, original_sid);
// The fork session should exist and have entries // 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()); assert!(!fork_entries.is_empty());
// Original session should still have the interloper entry // Original session should still have the interloper entry

View File

@ -1,4 +1,4 @@
//! Session-scoped TaskStore and builtin task tools. //! Pod-lifetime TaskStore and builtin task tools.
//! //!
//! The store is Pod/session-lifetime state shared by the four Task* tools. It //! The store is Pod/session-lifetime state shared by the four Task* tools. It
//! is reconstructed on resume by replaying TaskCreate / TaskUpdate tool-call //! is reconstructed on resume by replaying TaskCreate / TaskUpdate tool-call

View File

@ -1,4 +1,4 @@
//! Session-scoped tracker for file operations performed by the builtin //! Pod-lifetime tracker for file operations performed by the builtin
//! file-manipulation tools. //! file-manipulation tools.
//! //!
//! A `Tracker` serves two orthogonal purposes: //! A `Tracker` serves two orthogonal purposes:

View File

@ -483,7 +483,7 @@ impl App {
self.blocks.push(Block::UserMessage { segments }); self.blocks.push(Block::UserMessage { segments });
self.assistant_streaming = false; self.assistant_streaming = false;
} }
Event::SessionRotated { entry } => { Event::SegmentRotated { entry } => {
self.reset_for_rotation(); self.reset_for_rotation();
self.apply_log_entry_raw(&entry); self.apply_log_entry_raw(&entry);
self.assistant_streaming = false; self.assistant_streaming = false;
@ -685,7 +685,7 @@ impl App {
started_at: Instant::now(), started_at: Instant::now(),
})); }));
} }
Event::CompactDone { new_session_id } => { Event::CompactDone { new_segment_id } => {
if let Some(evt) = self.last_streaming_compact_mut() { if let Some(evt) = self.last_streaming_compact_mut() {
let elapsed_secs = match evt { let elapsed_secs = match evt {
CompactEvent::Streaming { started_at } => { CompactEvent::Streaming { started_at } => {
@ -694,12 +694,12 @@ impl App {
_ => None, _ => None,
}; };
*evt = CompactEvent::Done { *evt = CompactEvent::Done {
new_session_id, new_segment_id,
elapsed_secs, elapsed_secs,
}; };
} else { } else {
self.blocks.push(Block::Compact(CompactEvent::Done { self.blocks.push(Block::Compact(CompactEvent::Done {
new_session_id, new_segment_id,
elapsed_secs: None, elapsed_secs: None,
})); }));
} }
@ -932,7 +932,7 @@ impl App {
} }
/// Drop the derived view in preparation for replaying a new /// Drop the derived view in preparation for replaying a new
/// `SessionStart` (compaction / fork). Greeting is preserved /// `SegmentStart` (compaction / fork). Greeting is preserved
/// because the Pod identity hasn't changed. /// because the Pod identity hasn't changed.
fn reset_for_rotation(&mut self) { fn reset_for_rotation(&mut self) {
let greeting = self.blocks.iter().find_map(|b| match b { let greeting = self.blocks.iter().find_map(|b| match b {
@ -958,7 +958,7 @@ impl App {
return; return;
}; };
match entry { match entry {
session_store::LogEntry::SessionStart { history, .. } => { session_store::LogEntry::SegmentStart { history, .. } => {
for logged in history { for logged in history {
let item: llm_worker::Item = logged.into(); let item: llm_worker::Item = logged.into();
let item_value = serde_json::to_value(&item).expect("Item is Serialize"); let item_value = serde_json::to_value(&item).expect("Item is Serialize");
@ -1445,7 +1445,7 @@ mod completion_flow_tests {
#[test] #[test]
fn snapshot_renders_system_message_block_from_session_start() { fn snapshot_renders_system_message_block_from_session_start() {
let mut app = App::new("test".into()); let mut app = App::new("test".into());
let session_start = session_store::LogEntry::SessionStart { let session_start = session_store::LogEntry::SegmentStart {
ts: 1, ts: 1,
system_prompt: None, system_prompt: None,
config: Default::default(), config: Default::default(),
@ -1525,15 +1525,15 @@ mod completion_flow_tests {
let id = uuid::Uuid::parse_str("12345678-1234-5678-1234-567812345678").unwrap(); let id = uuid::Uuid::parse_str("12345678-1234-5678-1234-567812345678").unwrap();
app.handle_pod_event(Event::CompactStart); app.handle_pod_event(Event::CompactStart);
app.handle_pod_event(Event::CompactDone { new_session_id: id }); app.handle_pod_event(Event::CompactDone { new_segment_id: id });
assert_eq!(compact_block_count(&app), 1); assert_eq!(compact_block_count(&app), 1);
assert!(matches!( assert!(matches!(
app.blocks.as_slice(), app.blocks.as_slice(),
[Block::Compact(CompactEvent::Done { [Block::Compact(CompactEvent::Done {
new_session_id, new_segment_id,
elapsed_secs: Some(_), elapsed_secs: Some(_),
})] if *new_session_id == id })] if *new_segment_id == id
)); ));
} }

View File

@ -83,7 +83,7 @@ pub enum CompactEvent {
Streaming { started_at: Instant }, Streaming { started_at: Instant },
/// Compaction ended cleanly with `CompactDone`. /// Compaction ended cleanly with `CompactDone`.
Done { Done {
new_session_id: uuid::Uuid, new_segment_id: uuid::Uuid,
elapsed_secs: Option<u64>, elapsed_secs: Option<u64>,
}, },
/// Compaction ended with `CompactFailed`. /// Compaction ended with `CompactFailed`.

View File

@ -25,7 +25,7 @@ use crossterm::terminal::{
use protocol::{Method, PodStatus}; use protocol::{Method, PodStatus};
use ratatui::Terminal; use ratatui::Terminal;
use ratatui::backend::CrosstermBackend; use ratatui::backend::CrosstermBackend;
use session_store::SessionId; use session_store::SegmentId;
use client::PodClient; use client::PodClient;
@ -56,7 +56,7 @@ enum Mode {
Resume, Resume,
/// `tui --session <UUID>`: skip the picker, go straight to the /// `tui --session <UUID>`: skip the picker, go straight to the
/// resume name dialog with `id` baked in. /// resume name dialog with `id` baked in.
ResumeWithSession(SessionId), ResumeWithSession(SegmentId),
} }
enum ParseError { enum ParseError {
@ -78,7 +78,7 @@ impl std::fmt::Display for ParseError {
fn parse_args() -> Result<Mode, ParseError> { fn parse_args() -> Result<Mode, ParseError> {
let args: Vec<String> = std::env::args().skip(1).collect(); let args: Vec<String> = std::env::args().skip(1).collect();
let mut resume = false; let mut resume = false;
let mut session: Option<SessionId> = None; let mut session: Option<SegmentId> = None;
let mut socket_override: Option<PathBuf> = None; let mut socket_override: Option<PathBuf> = None;
let mut positional: Option<String> = None; let mut positional: Option<String> = None;
@ -94,7 +94,7 @@ fn parse_args() -> Result<Mode, ParseError> {
.get(i + 1) .get(i + 1)
.ok_or(ParseError::MissingValue("--session"))?; .ok_or(ParseError::MissingValue("--session"))?;
session = Some( session = Some(
raw.parse::<SessionId>() raw.parse::<SegmentId>()
.map_err(|_| ParseError::InvalidSession(raw.clone()))?, .map_err(|_| ParseError::InvalidSession(raw.clone()))?,
); );
i += 2; i += 2;
@ -216,7 +216,7 @@ async fn run_resume() -> Result<(), Box<dyn std::error::Error>> {
run_spawn(Some(id)).await run_spawn(Some(id)).await
} }
async fn run_spawn(resume_from: Option<SessionId>) -> Result<(), Box<dyn std::error::Error>> { async fn run_spawn(resume_from: Option<SegmentId>) -> Result<(), Box<dyn std::error::Error>> {
let ready = match spawn::run(resume_from).await? { let ready = match spawn::run(resume_from).await? {
SpawnOutcome::Ready(r) => r, SpawnOutcome::Ready(r) => r,
SpawnOutcome::Cancelled => return Ok(()), SpawnOutcome::Cancelled => return Ok(()),

View File

@ -2,7 +2,7 @@
//! //!
//! Reads the most recent sessions from the configured store, lets the //! Reads the most recent sessions from the configured store, lets the
//! user pick one with the arrow keys, and returns the chosen //! user pick one with the arrow keys, and returns the chosen
//! `SessionId`. Closes its inline viewport before returning so the //! `SegmentId`. Closes its inline viewport before returning so the
//! caller can open a fresh viewport for the name dialog. //! caller can open a fresh viewport for the name dialog.
//! //!
//! The picker only handles selection. Forking, pod-registry checks, and //! The picker only handles selection. Forking, pod-registry checks, and
@ -12,7 +12,7 @@ use std::io;
use std::time::Duration; use std::time::Duration;
use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind, KeyModifiers}; use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind, KeyModifiers};
use pod_registry::lookup_session; use pod_registry::lookup_segment;
use ratatui::Terminal; use ratatui::Terminal;
use ratatui::backend::CrosstermBackend; use ratatui::backend::CrosstermBackend;
use ratatui::layout::{Constraint, Layout}; use ratatui::layout::{Constraint, Layout};
@ -20,7 +20,7 @@ use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span}; use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph; use ratatui::widgets::Paragraph;
use ratatui::{Frame, TerminalOptions, Viewport}; use ratatui::{Frame, TerminalOptions, Viewport};
use session_store::{FsStore, LogEntry, LoggedContentPart, LoggedItem, SessionId, Store}; use session_store::{FsStore, LogEntry, LoggedContentPart, LoggedItem, SegmentId, Store};
const MAX_ROWS: usize = 10; const MAX_ROWS: usize = 10;
const VIEWPORT_LINES: u16 = MAX_ROWS as u16 + 4; const VIEWPORT_LINES: u16 = MAX_ROWS as u16 + 4;
@ -60,26 +60,26 @@ impl From<session_store::StoreError> for PickerError {
} }
pub enum PickerOutcome { pub enum PickerOutcome {
Picked(SessionId), Picked(SegmentId),
Cancelled, Cancelled,
} }
/// One row in the picker view. Rendered from the session log so the /// One row in the picker view. Rendered from the session log so the
/// user can recognise their session at a glance without parsing UUIDs. /// user can recognise their session at a glance without parsing UUIDs.
struct Row { struct Row {
id: SessionId, id: SegmentId,
/// Last user / assistant snippet, or a `[corrupt]` placeholder. /// Last user / assistant snippet, or a `[corrupt]` placeholder.
preview: String, preview: String,
/// `Some(pod_name)` when a live Pod currently holds an allocation /// `Some(pod_name)` when a live Pod currently holds an allocation
/// for this session in `pods.json`. Picking such a row launches /// for this session in `pods.json`. Picking such a row launches
/// `pod --session <UUID>` which will fail with `SessionConflict` — /// `pod --session <UUID>` which will fail with `SegmentConflict` —
/// the badge warns the user up-front. /// the badge warns the user up-front.
live_pod: Option<String>, live_pod: Option<String>,
} }
pub async fn run() -> Result<PickerOutcome, PickerError> { pub async fn run() -> Result<PickerOutcome, PickerError> {
let store = open_default_store()?; let store = open_default_store()?;
let ids = store.list_sessions()?; let ids = store.list_segments()?;
if ids.is_empty() { if ids.is_empty() {
return Err(PickerError::NoSessions); return Err(PickerError::NoSessions);
} }
@ -89,7 +89,7 @@ pub async fn run() -> Result<PickerOutcome, PickerError> {
// Best-effort live check. A pods.json I/O hiccup downgrades // Best-effort live check. A pods.json I/O hiccup downgrades
// the row to "no badge" rather than killing the picker — the // the row to "no badge" rather than killing the picker — the
// user still gets to see the listing. // user still gets to see the listing.
let live_pod = lookup_session(id).ok().flatten().map(|info| info.pod_name); let live_pod = lookup_segment(id).ok().flatten().map(|info| info.pod_name);
rows.push(Row { rows.push(Row {
id, id,
preview, preview,
@ -158,7 +158,7 @@ fn open_default_store() -> Result<FsStore, PickerError> {
Ok(FsStore::new(&dir)?) Ok(FsStore::new(&dir)?)
} }
fn build_preview(store: &FsStore, id: SessionId) -> String { fn build_preview(store: &FsStore, id: SegmentId) -> String {
match store.read_all(id) { match store.read_all(id) {
Ok(entries) => last_message_preview(&entries).unwrap_or_else(|| "[empty]".to_string()), Ok(entries) => last_message_preview(&entries).unwrap_or_else(|| "[empty]".to_string()),
Err(_) => "[corrupt]".to_string(), Err(_) => "[corrupt]".to_string(),
@ -300,7 +300,7 @@ fn row_line(row: &Row, selected: bool) -> Line<'_> {
}; };
let mut spans = vec![ let mut spans = vec![
Span::raw(marker), Span::raw(marker),
Span::styled(short_session(row.id), id_style), Span::styled(short_segment(row.id), id_style),
Span::raw(" "), Span::raw(" "),
]; ];
if let Some(ref pod_name) = row.live_pod { if let Some(ref pod_name) = row.live_pod {
@ -313,7 +313,7 @@ fn row_line(row: &Row, selected: bool) -> Line<'_> {
Line::from(spans) Line::from(spans)
} }
fn short_session(id: SessionId) -> String { fn short_segment(id: SegmentId) -> String {
let s = id.to_string(); let s = id.to_string();
s.chars().take(8).collect() s.chars().take(8).collect()
} }

View File

@ -28,7 +28,7 @@ use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span}; use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph; use ratatui::widgets::Paragraph;
use ratatui::{Frame, TerminalOptions, Viewport}; use ratatui::{Frame, TerminalOptions, Viewport};
use session_store::SessionId; use session_store::SegmentId;
const VIEWPORT_LINES: u16 = 6; const VIEWPORT_LINES: u16 = 6;
@ -46,7 +46,7 @@ pub enum SpawnOutcome {
pub enum SpawnError { pub enum SpawnError {
Io(io::Error), Io(io::Error),
Store(session_store::StoreError), Store(session_store::StoreError),
MissingResumeScope { session_id: SessionId }, MissingResumeScope { segment_id: SegmentId },
Spawn(client::SpawnError), Spawn(client::SpawnError),
} }
@ -55,9 +55,9 @@ impl std::fmt::Display for SpawnError {
match self { match self {
Self::Io(e) => write!(f, "io error: {e}"), Self::Io(e) => write!(f, "io error: {e}"),
Self::Store(e) => write!(f, "failed to read session log: {e}"), Self::Store(e) => write!(f, "failed to read session log: {e}"),
Self::MissingResumeScope { session_id } => write!( Self::MissingResumeScope { segment_id } => write!(
f, f,
"session {session_id} has no persisted scope snapshot; refusing resume without explicit scope" "session {segment_id} has no persisted scope snapshot; refusing resume without explicit scope"
), ),
Self::Spawn(e) => write!(f, "{e}"), Self::Spawn(e) => write!(f, "{e}"),
} }
@ -89,7 +89,7 @@ type InlineTerminal = Terminal<CrosstermBackend<io::Stdout>>;
/// Source session for a resume run. `None` = fresh spawn (current /// Source session for a resume run. `None` = fresh spawn (current
/// behaviour); `Some(id)` swaps the dialog into "Resume Pod" mode and /// behaviour); `Some(id)` swaps the dialog into "Resume Pod" mode and
/// passes `--session <id>` to the spawned `pod` child. /// passes `--session <id>` to the spawned `pod` child.
pub async fn run(resume_from: Option<SessionId>) -> Result<SpawnOutcome, SpawnError> { pub async fn run(resume_from: Option<SegmentId>) -> Result<SpawnOutcome, SpawnError> {
let cwd = std::env::current_dir().map_err(SpawnError::Io)?; let cwd = std::env::current_dir().map_err(SpawnError::Io)?;
// Run the same merge pod itself uses, then read what's missing // Run the same merge pod itself uses, then read what's missing
@ -321,7 +321,7 @@ fn build_overlay_toml(form: &Form) -> String {
toml::to_string(&toml::Value::Table(root)).expect("overlay serialisation cannot fail") toml::to_string(&toml::Value::Table(root)).expect("overlay serialisation cannot fail")
} }
async fn load_resume_scope(session_id: SessionId) -> Result<ScopeConfig, SpawnError> { async fn load_resume_scope(segment_id: SegmentId) -> Result<ScopeConfig, SpawnError> {
let store_dir = manifest::paths::sessions_dir().ok_or_else(|| { let store_dir = manifest::paths::sessions_dir().ok_or_else(|| {
io::Error::new( io::Error::new(
io::ErrorKind::NotFound, io::ErrorKind::NotFound,
@ -329,10 +329,10 @@ async fn load_resume_scope(session_id: SessionId) -> Result<ScopeConfig, SpawnEr
) )
})?; })?;
let store = session_store::FsStore::new(&store_dir)?; let store = session_store::FsStore::new(&store_dir)?;
let state = session_store::restore(&store, session_id)?; let state = session_store::restore(&store, segment_id)?;
let snapshot = state let snapshot = state
.pod_scope .pod_scope
.ok_or(SpawnError::MissingResumeScope { session_id })?; .ok_or(SpawnError::MissingResumeScope { segment_id })?;
Ok(ScopeConfig { Ok(ScopeConfig {
allow: snapshot.allow, allow: snapshot.allow,
deny: snapshot.deny, deny: snapshot.deny,
@ -376,7 +376,7 @@ struct Form {
/// switches, the source session is shown to the user, and the /// switches, the source session is shown to the user, and the
/// child pod is launched with `--session <id>` so it restores /// child pod is launched with `--session <id>` so it restores
/// from `id` and appends to the same session log. /// from `id` and appends to the same session log.
resume_from: Option<SessionId>, resume_from: Option<SegmentId>,
/// Scope snapshot recovered from the source session log. Set only for /// Scope snapshot recovered from the source session log. Set only for
/// resume runs, and serialized into the overlay instead of cwd-default /// resume runs, and serialized into the overlay instead of cwd-default
/// scope so resume does not silently broaden access. /// scope so resume does not silently broaden access.
@ -445,7 +445,7 @@ fn draw_form(f: &mut Frame<'_>, form: &Form) {
.split(area); .split(area);
let title_text = match form.resume_from { let title_text = match form.resume_from {
Some(id) => format!("resume pod session: {}", short_session(id)), Some(id) => format!("resume pod session: {}", short_segment(id)),
None => "spawn pod".to_string(), None => "spawn pod".to_string(),
}; };
let title = Paragraph::new(Line::from(vec![Span::styled( let title = Paragraph::new(Line::from(vec![Span::styled(
@ -473,7 +473,7 @@ fn draw_form(f: &mut Frame<'_>, form: &Form) {
/// First 8 hex digits of a UUID — short enough to skim, long enough /// First 8 hex digits of a UUID — short enough to skim, long enough
/// to disambiguate inside a 10-row picker. /// to disambiguate inside a 10-row picker.
pub(crate) fn short_session(id: SessionId) -> String { pub(crate) fn short_segment(id: SegmentId) -> String {
let s = id.to_string(); let s = id.to_string();
s.chars().take(8).collect() s.chars().take(8).collect()
} }
@ -584,7 +584,7 @@ mod tests {
#[test] #[test]
fn overlay_uses_resume_scope_snapshot() { fn overlay_uses_resume_scope_snapshot() {
let mut f = form("agent-r", false); let mut f = form("agent-r", false);
f.resume_from = Some(session_store::new_session_id()); f.resume_from = Some(session_store::new_segment_id());
f.resume_scope = Some(ScopeConfig { f.resume_scope = Some(ScopeConfig {
allow: vec![manifest::ScopeRule { allow: vec![manifest::ScopeRule {
target: PathBuf::from("/work/example"), target: PathBuf::from("/work/example"),

View File

@ -1019,10 +1019,10 @@ fn render_compact(lines: &mut Vec<Line<'static>>, evt: &CompactEvent, width: u16
) )
} }
CompactEvent::Done { CompactEvent::Done {
new_session_id, new_segment_id,
elapsed_secs, elapsed_secs,
} => { } => {
let short = new_session_id let short = new_segment_id
.to_string() .to_string()
.chars() .chars()
.take(8) .take(8)

View File

@ -1,35 +0,0 @@
# session-store: SessionId → SegmentId へのリネーム
## 背景
永続化層の現 `SessionId` は、append-only log の物理的 / 復元上の単位を指しているcompaction や fork で新 ID が発行される)。一方ユーザー視点では「同じ会話の継続」が compaction / fork を跨いで成立しており、ここに**ユーザー視点の会話の家系 = `Session`** と **物理 append-only 単位 = `Segment`** の 2 階層を導入したい。
本チケットは 2 階層導入のうち、**先に物理単位側のリネームだけ**を機械的に済ませる。新 `SessionId`grouping 概念)は次チケットで導入する。
## 要件
- `crates/session-store/` 内の `SessionId` 型・関連関数 (`SessionLogWriter` / `Store::append` / `fork` / `fork_at` 等) を `SegmentId` に統一。
- 同様に `pod` / `pod-registry` / `pod-cli` / `llm-worker` 関連の呼び出し元の参照を `SegmentId` に追従。
- `SessionLogWriter` / `SessionStart` / `SessionOrigin` 等の `Session` プレフィックス型のうち、**segment レベルを指しているもの**は同時に `Segment` プレフィックスへ変える。
- 線引きの基準: そのデータが「fork / compaction で新規生成される log 1 本」に紐づくなら `Segment`。会話の家系全体に紐づく概念は何も無いはずなので(次チケットで初めて出てくる)、本チケット内では全て Segment 系に倒す。
- crate 名 (`session-store`) の rename 要否は本チケットでは扱わず保留。中身が Segment 中心になった事実のみで判断材料を残す。
- `--session <UUID>` 系 CLI 引数は内部実装としては `SegmentId` を受けるが、外部表記は変更しない(ユーザー向け ID 体系の整理は `session-grouping-introduce` で扱う)。
## 完了条件
- `SessionId` 型がコードベースに残らない(後続の Session grouping で導入する新 `SessionId` は無関係)。
- `cargo check --workspace` および全テストが通る。
- 既存 session の JSONL を Segment 中心の API で読み書きできる。
## 範囲外
- 新 `SessionId` (Segment 群の grouping) の導入(次チケット `session-grouping-introduce`)。
- session-store crate 名の rename。
- 外部公開 CLI 引数の rename。
## 関連
- `tickets/entry-hash-abolish.md` (前提)
- `tickets/session-grouping-introduce.md` (後続)
- `crates/session-store/`
- `crates/pod/src/pod.rs`