feat: Session(Segment 群の grouping)を導入

- SessionId 型を新設、各 SegmentStart に session_id を持たせる
- compaction / 内部 fork は同 SessionId を継承、fork() は新 Session を発行
- Store API を (SessionId, SegmentId) ベースに、FsStore layout は
  <root>/<session_id>/<segment_id>.jsonl に
- Store::list_sessions / list_segments(session_id) / lookup_session_of を追加
- restore_by_segment shim を session-store に提供(pod-cli --session で使用)
- SegmentState に SegmentLocation (session_id, segment_id) を保持し ArcSwap で更新
- RestoredState に session_id: Option<SessionId> を追加
- Picker は Session 単位に列挙、leaf segment を解決して resume
This commit is contained in:
2026-05-20 06:17:56 +09:00
parent d2b3c2f53d
commit 5edc4d3b03
18 changed files with 715 additions and 316 deletions
+23 -2
View File
@@ -5,7 +5,7 @@ use std::process::ExitCode;
use clap::Parser;
use manifest::{PodManifest, paths};
use pod::{Pod, PodController, PodFactory, PromptLoader};
use session_store::{FsStore, SegmentId};
use session_store::{FsStore, SegmentId, Store};
const USER_MANIFEST_ENV: &str = "INSOMNIA_USER_MANIFEST";
@@ -186,7 +186,28 @@ async fn main() -> ExitCode {
}
}
} else if let Some(source_segment_id) = cli.session {
match Pod::restore_from_manifest(source_segment_id, manifest, store, loader).await {
let source_session_id = match store.lookup_session_of(source_segment_id) {
Ok(Some(sid)) => sid,
Ok(None) => {
eprintln!(
"error: --session {source_segment_id}: segment is not registered to any session"
);
return ExitCode::FAILURE;
}
Err(e) => {
eprintln!("error: lookup_session_of failed: {e}");
return ExitCode::FAILURE;
}
};
match Pod::restore_from_manifest(
source_session_id,
source_segment_id,
manifest,
store,
loader,
)
.await
{
Ok(p) => p,
Err(e) => {
eprintln!("error: failed to restore pod: {e}");
+94 -48
View File
@@ -9,7 +9,8 @@ use llm_worker::llm_client::client::LlmClient;
use llm_worker::state::Mutable;
use llm_worker::{ToolOutputLimits, UsageRecord, Worker, WorkerError, WorkerResult};
use session_store::{
LogEntry, PodScopeSnapshot, SegmentId, Store, StoreError, SystemItem, segment_log, to_logged,
LogEntry, PodScopeSnapshot, SegmentId, SessionId, Store, StoreError, SystemItem, segment_log,
to_logged,
};
use tracing::{info, warn};
@@ -42,35 +43,54 @@ use protocol::{AlertLevel, AlertSource, Event, Segment};
use tokio::sync::broadcast;
use tokio::task::JoinHandle;
/// Lock-free shared session pointer.
/// `(SessionId, SegmentId)` pair the Pod is currently writing to.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SegmentLocation {
pub session_id: SessionId,
pub segment_id: SegmentId,
}
/// Lock-free shared session/segment pointer.
///
/// Holds the current `(segment_id, entries_written)` pair so that the
/// Pod and every `LogWriterHandle` clone see a consistent view through
/// `Arc`-shared lock-free reads. `segment_id` is wrapped in `ArcSwap`
/// so fork (a rare, run-start-only event) can atomically swap it
/// without taking a mutex on the append hot path. `entries_written` is
/// an `AtomicUsize` bumped on every successful append; the writer's
/// tally is compared against the store's on-disk count to detect
/// concurrent writers in `ensure_segment_head`.
/// Holds the current `(SessionId, SegmentId)` pair and the append tally
/// so that the Pod and every `LogWriterHandle` clone see a consistent
/// view through `Arc`-shared lock-free reads. The location is wrapped in
/// `ArcSwap` so fork (a rare, run-start-only event) can atomically swap
/// session_id + segment_id together without taking a mutex on the
/// append hot path. `entries_written` is an `AtomicUsize` bumped on
/// every successful append; the writer's tally is compared against the
/// store's on-disk count to detect concurrent writers in
/// `ensure_segment_head`.
pub struct SegmentState {
segment_id: ArcSwap<SegmentId>,
location: ArcSwap<SegmentLocation>,
entries_written: AtomicUsize,
}
impl SegmentState {
pub fn new(segment_id: SegmentId, entries_written: usize) -> Arc<Self> {
pub fn new(session_id: SessionId, segment_id: SegmentId, entries_written: usize) -> Arc<Self> {
Arc::new(Self {
segment_id: ArcSwap::from_pointee(segment_id),
location: ArcSwap::from_pointee(SegmentLocation {
session_id,
segment_id,
}),
entries_written: AtomicUsize::new(entries_written),
})
}
pub fn segment_id(&self) -> SegmentId {
**self.segment_id.load()
pub fn location(&self) -> SegmentLocation {
**self.location.load()
}
pub fn set_segment_id(&self, id: SegmentId) {
self.segment_id.store(Arc::new(id));
pub fn session_id(&self) -> SessionId {
self.location().session_id
}
pub fn segment_id(&self) -> SegmentId {
self.location().segment_id
}
pub fn set_location(&self, loc: SegmentLocation) {
self.location.store(Arc::new(loc));
}
pub fn entries_written(&self) -> usize {
@@ -107,8 +127,8 @@ where
/// writes for `< PIPE_BUF` lines, so no user-space serialization is
/// needed across appenders.
pub fn append_entry(&self, entry: LogEntry) -> Result<(), StoreError> {
let segment_id = self.state.segment_id();
self.store.append(segment_id, &entry)?;
let loc = self.state.location();
self.store.append(loc.session_id, loc.segment_id, &entry)?;
self.state.increment_entries();
self.sink.publish(entry);
Ok(())
@@ -472,13 +492,14 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
// Segment creation is deferred to `ensure_segment_head` at first
// run so a later-installed system-prompt template (see
// `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 mut pod = Self {
manifest,
worker: Some(worker),
store,
segment_state: SegmentState::new(segment_id, 0),
segment_state: SegmentState::new(session_id, segment_id, 0),
pwd,
scope: SharedScope::new(scope),
hook_builder: HookRegistryBuilder::new(),
@@ -542,12 +563,18 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
&self.prompts
}
/// The session ID used for persistence. Read lock-free from the
/// shared session pointer so fork-time swaps are observed immediately.
/// The current segment ID. Read lock-free from the shared session
/// pointer so fork-time swaps are observed immediately.
pub fn segment_id(&self) -> SegmentId {
self.segment_state.segment_id()
}
/// The Session this Pod belongs to. Stable across compaction and
/// in-Session fork — only `fork` (a brand-new Session) changes it.
pub fn session_id(&self) -> SessionId {
self.segment_state.session_id()
}
/// The Pod's manifest.
pub fn manifest(&self) -> &PodManifest {
&self.manifest
@@ -627,8 +654,8 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
/// concurrent appenders — the kernel orders `O_APPEND` writes for
/// lines smaller than `PIPE_BUF`.
pub(crate) fn commit_entry(&self, entry: LogEntry) -> Result<(), StoreError> {
let segment_id = self.segment_state.segment_id();
self.store.append(segment_id, &entry)?;
let loc = self.segment_state.location();
self.store.append(loc.session_id, loc.segment_id, &entry)?;
self.segment_state.increment_entries();
self.sink.publish(entry);
Ok(())
@@ -1618,11 +1645,12 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
/// when another writer has appended behind our back.
fn ensure_segment_head(&mut self) -> Result<(), PodError> {
let w = self.worker.as_ref().unwrap();
let prev_segment_id = self.segment_state.segment_id();
let loc = self.segment_state.location();
let entries_written = self.segment_state.entries_written();
if entries_written == 0 {
let initial = LogEntry::SegmentStart {
ts: segment_log::now_millis(),
session_id: loc.session_id,
system_prompt: w.get_system_prompt().map(String::from),
config: w.request_config().clone(),
history: to_logged(w.history()),
@@ -1636,17 +1664,19 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
// Check store count + auto-fork if it drifted.
let store_count = self
.store
.read_entry_count(prev_segment_id)
.read_entry_count(loc.session_id, loc.segment_id)
.map_err(PodError::from)?;
if store_count == entries_written {
return Ok(());
}
// Fork: mint a fresh session and switch to it. The new
// SegmentStart entry replaces the mirror and is broadcast
// through the sink so existing subscribers reset their view.
let fork_id = session_store::new_segment_id();
// Auto-fork within the same Session: mint a fresh Segment and
// switch to it. The new SegmentStart entry replaces the mirror
// and is broadcast through the sink so existing subscribers
// reset their view.
let fork_segment_id = session_store::new_segment_id();
let entry = LogEntry::SegmentStart {
ts: segment_log::now_millis(),
session_id: loc.session_id,
system_prompt: w.get_system_prompt().map(String::from),
config: w.request_config().clone(),
history: to_logged(w.history()),
@@ -1654,13 +1684,16 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
compacted_from: None,
};
self.store
.create_segment(fork_id, &[entry.clone()])
.create_segment(loc.session_id, fork_segment_id, &[entry.clone()])
.map_err(PodError::from)?;
self.segment_state.set_segment_id(fork_id);
self.segment_state.set_location(SegmentLocation {
session_id: loc.session_id,
segment_id: fork_segment_id,
});
self.segment_state.set_entries_written(1);
self.sink.reset_with_initial(entry);
if self.scope_allocation.is_some() {
pod_registry::update_segment(&self.manifest.pod.name, fork_id)?;
pod_registry::update_segment(&self.manifest.pod.name, fork_segment_id)?;
}
Ok(())
}
@@ -2140,27 +2173,34 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
task_snapshot_text.clone(),
));
// Build the SegmentStart entry for the new compacted session,
// then atomically rotate to it: create on disk, swap head, reset
// the broadcast sink so existing subscribers see the new
// `SegmentStart { compacted_from }` and reset their view.
// Build the SegmentStart entry for the new compacted segment.
// Inherits the source Segment's session_id so the compacted
// lineage stays grouped under the same Session. Atomically
// rotate: create on disk, swap location, reset the broadcast
// sink so existing subscribers see the new `SegmentStart
// { compacted_from }` and reset their view.
let new_segment_id = session_store::new_segment_id();
let old_session_id = self.segment_state.segment_id();
let old_loc = self.segment_state.location();
let source_turn_count = self.worker.as_ref().unwrap().turn_count();
let w = self.worker.as_ref().unwrap();
let entry = LogEntry::SegmentStart {
ts: segment_log::now_millis(),
session_id: old_loc.session_id,
system_prompt: w.get_system_prompt().map(String::from),
config: w.request_config().clone(),
history: to_logged(&new_history),
forked_from: None,
compacted_from: Some(session_store::SegmentOrigin {
segment_id: old_session_id,
segment_id: old_loc.segment_id,
at_turn_index: source_turn_count,
}),
};
self.store.create_segment(new_segment_id, &[entry.clone()])?;
self.segment_state.set_segment_id(new_segment_id);
self.store
.create_segment(old_loc.session_id, new_segment_id, &[entry.clone()])?;
self.segment_state.set_location(SegmentLocation {
session_id: old_loc.session_id,
segment_id: new_segment_id,
});
self.segment_state.set_entries_written(1);
let session_start = entry;
// Broadcast the SegmentStart through the sink. This atomically
@@ -2367,7 +2407,10 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
// Read the session log to get the current entry count. This is
// the boundary for the source.range end_entry. Called once per
// extract, on a small local file.
let entries_now = self.store.read_all(self.segment_id())?.len();
let entries_now = self
.store
.read_all(self.session_id(), self.segment_id())?
.len();
if entries_now == 0 {
return Ok(ExtractDecision::Skipped);
}
@@ -2738,8 +2781,9 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
// Segment creation is deferred to the first run (see
// `ensure_segment_head`) so the SegmentStart entry can capture
// the rendered system prompt, not the raw template source. The
// segment_id is allocated here so the pod-registry registration
// can record it from the start.
// session_id + segment_id are allocated here so the pod-registry
// registration can record them 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
@@ -2765,7 +2809,7 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
manifest,
worker: Some(worker),
store,
segment_state: SegmentState::new(segment_id, 0),
segment_state: SegmentState::new(session_id, segment_id, 0),
pwd: common.pwd,
scope: SharedScope::new(common.scope),
hook_builder: HookRegistryBuilder::new(),
@@ -2820,6 +2864,7 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
let mut common = prepare_pod_common(&manifest, &loader, /* parse_template */ true)?;
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(
manifest.pod.name.clone(),
@@ -2835,7 +2880,7 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
manifest,
worker: Some(worker),
store,
segment_state: SegmentState::new(segment_id, 0),
segment_state: SegmentState::new(session_id, segment_id, 0),
pwd: common.pwd,
scope: SharedScope::new(common.scope),
hook_builder: HookRegistryBuilder::new(),
@@ -2892,6 +2937,7 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
/// session keeps a stable cache prefix even when the manifest's
/// instruction template would render differently today.
pub async fn restore_from_manifest(
session_id: SessionId,
segment_id: SegmentId,
manifest: PodManifest,
store: St,
@@ -2900,7 +2946,7 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
// Read raw entries once so we can both reconstruct state and
// seed the broadcast sink's mirror with the same prefix that
// sits on disk.
let raw_entries = store.read_all(segment_id)?;
let raw_entries = store.read_all(session_id, segment_id)?;
let state = session_store::collect_state(&raw_entries);
if state.entries_count == 0 {
return Err(PodError::SegmentEmpty { segment_id });
@@ -2974,7 +3020,7 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
manifest,
worker: Some(worker),
store,
segment_state: SegmentState::new(segment_id, state.entries_count),
segment_state: SegmentState::new(session_id, segment_id, state.entries_count),
pwd: common.pwd,
scope: SharedScope::new(common.scope),
hook_builder: HookRegistryBuilder::new(),
+1
View File
@@ -203,6 +203,7 @@ mod tests {
fn session_start() -> LogEntry {
LogEntry::SegmentStart {
ts: now_millis(),
session_id: uuid::Uuid::nil(),
system_prompt: None,
config: RequestConfig::default(),
history: vec![],
+48 -38
View File
@@ -8,7 +8,7 @@
use std::sync::{LazyLock, Mutex};
use pod::{Pod, PodError};
use session_store::{FsStore, SegmentId, StoreError};
use session_store::{FsStore, StoreError};
const MINIMAL_MANIFEST_TOML: &str = r#"
[pod]
@@ -32,7 +32,7 @@ permission = "write"
static ENV_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
#[tokio::test]
async fn restore_from_manifest_rejects_unknown_session() {
async fn restore_from_manifest_rejects_unknown_segment() {
let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let store_tmp = tempfile::tempdir().unwrap();
@@ -42,66 +42,76 @@ async fn restore_from_manifest_rejects_unknown_session() {
// A freshly-minted id with no jsonl file at all → store returns
// NotFound, which `Pod::restore_from_manifest` surfaces verbatim
// as `PodError::Store`.
let unknown = session_store::new_segment_id();
let unknown_sid = session_store::new_session_id();
let unknown_seg = session_store::new_segment_id();
let result = Pod::restore_from_manifest(
unknown_sid,
unknown_seg,
manifest,
store,
pod::PromptLoader::builtins_only(),
)
.await;
match result {
Err(PodError::Store(StoreError::NotFound(id))) => assert_eq!(id, unknown_seg),
Err(other) => panic!("expected Store(NotFound), got {other:?}"),
Ok(_) => panic!("expected unknown segment to fail"),
}
}
#[tokio::test]
async fn restore_from_manifest_rejects_empty_segment_log() {
let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let store_tmp = tempfile::tempdir().unwrap();
let store = FsStore::new(store_tmp.path()).unwrap();
let manifest = pod::PodManifest::from_toml(MINIMAL_MANIFEST_TOML).unwrap();
// Pre-create an empty `<sid>/<segid>.jsonl` so `read_all` succeeds
// with no entries. `collect_state` returns `entries_count = 0`,
// which `restore_from_manifest` rejects with `SegmentEmpty` *before*
// it gets as far as building the LLM client.
let sid = session_store::new_session_id();
let segid = session_store::new_segment_id();
let dir = store_tmp.path().join(sid.to_string());
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join(format!("{segid}.jsonl")), b"").unwrap();
let result =
Pod::restore_from_manifest(unknown, manifest, store, pod::PromptLoader::builtins_only())
Pod::restore_from_manifest(sid, segid, manifest, store, pod::PromptLoader::builtins_only())
.await;
match result {
Err(PodError::Store(StoreError::NotFound(id))) => assert_eq!(id, unknown),
Err(other) => panic!("expected Store(NotFound), got {other:?}"),
Ok(_) => panic!("expected unknown session to fail"),
}
}
#[tokio::test]
async fn restore_from_manifest_rejects_empty_session_log() {
let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let store_tmp = tempfile::tempdir().unwrap();
let store = FsStore::new(store_tmp.path()).unwrap();
let manifest = pod::PodManifest::from_toml(MINIMAL_MANIFEST_TOML).unwrap();
// Pre-create an empty `<id>.jsonl` so `read_all` succeeds with no
// entries. `collect_state` returns `entries_count = 0`, which
// `restore_from_manifest` rejects with `SegmentEmpty` *before* it
// gets as far as building the LLM client — so the test does not
// need credentials or a runtime sandbox.
let id: SegmentId = session_store::new_segment_id();
let path = store_tmp.path().join(format!("{id}.jsonl"));
std::fs::write(&path, b"").unwrap();
let result =
Pod::restore_from_manifest(id, manifest, store, pod::PromptLoader::builtins_only()).await;
match result {
Err(PodError::SegmentEmpty { segment_id }) => assert_eq!(segment_id, id),
Err(PodError::SegmentEmpty { segment_id }) => assert_eq!(segment_id, segid),
Err(other) => panic!("expected SegmentEmpty, got {other:?}"),
Ok(_) => panic!("expected empty session log to fail"),
Ok(_) => panic!("expected empty segment log to fail"),
}
}
#[tokio::test]
async fn restore_from_manifest_rejects_session_without_scope_snapshot() {
async fn restore_from_manifest_rejects_segment_without_scope_snapshot() {
let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let store_tmp = tempfile::tempdir().unwrap();
let store = FsStore::new(store_tmp.path()).unwrap();
let manifest = pod::PodManifest::from_toml(MINIMAL_MANIFEST_TOML).unwrap();
let id = session_store::new_segment_id();
let sid = session_store::new_session_id();
let segid = session_store::new_segment_id();
let state = session_store::SegmentStartState {
system_prompt: None,
config: &Default::default(),
history: &[],
};
session_store::create_segment_with_id(&store, id, state).unwrap();
session_store::create_segment_with_ids(&store, sid, segid, state).unwrap();
let result =
Pod::restore_from_manifest(id, manifest, store, pod::PromptLoader::builtins_only()).await;
Pod::restore_from_manifest(sid, segid, manifest, store, pod::PromptLoader::builtins_only())
.await;
match result {
Err(PodError::SegmentScopeMissing { segment_id }) => assert_eq!(segment_id, id),
Err(PodError::SegmentScopeMissing { segment_id }) => assert_eq!(segment_id, segid),
Err(other) => panic!("expected SegmentScopeMissing, got {other:?}"),
Ok(_) => panic!("expected missing scope snapshot to fail"),
}
+52 -19
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::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
use session_metrics::{DOMAIN, Metric, metrics_from_extensions};
use session_store::{FsStore, LogEntry, SegmentId, Store, StoreError, TraceEntry};
use session_store::{FsStore, LogEntry, SegmentId, SessionId, Store, StoreError, TraceEntry};
use pod::{Pod, PodManifest};
@@ -200,6 +200,7 @@ async fn prune_metrics_emit_skip_then_fire_with_post_request_join() {
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 session_id = pod.session_id();
let segment_id = pod.segment_id();
// Cloning the store handle to read the session log back after the
// runs complete — the Pod retains its own copy.
@@ -208,7 +209,7 @@ async fn prune_metrics_emit_skip_then_fire_with_post_request_join() {
pod.run_text("first").await.unwrap();
pod.run_text("second").await.unwrap();
let state = session_store::restore(&store, segment_id).unwrap();
let state = session_store::restore(&store, session_id, segment_id).unwrap();
let metrics = metrics_from_extensions(&state.extensions);
// Run 1 has 2 LLM iterations (tool loop), each evaluates prune with
@@ -288,13 +289,14 @@ async fn prune_metrics_record_below_min_savings_skip() {
]);
let (mut pod, _store_tmp, _pwd_tmp) =
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();
pod.run_text("first").await.unwrap();
pod.run_text("second").await.unwrap();
let state = session_store::restore(&store, segment_id).unwrap();
let state = session_store::restore(&store, session_id, segment_id).unwrap();
let metrics = metrics_from_extensions(&state.extensions);
let below = metrics
.iter()
@@ -327,31 +329,60 @@ struct MetricFailingStore {
}
impl Store for MetricFailingStore {
fn append(&self, id: SegmentId, entry: &LogEntry) -> Result<(), StoreError> {
fn append(
&self,
session_id: SessionId,
segment_id: SegmentId,
entry: &LogEntry,
) -> Result<(), StoreError> {
if let LogEntry::Extension { domain, .. } = entry {
if domain == DOMAIN {
return Err(StoreError::Io(std::io::Error::other("synthetic failure")));
}
}
self.inner.append(id, entry)
self.inner.append(session_id, segment_id, entry)
}
fn read_all(&self, id: SegmentId) -> Result<Vec<LogEntry>, StoreError> {
self.inner.read_all(id)
fn read_all(
&self,
session_id: SessionId,
segment_id: SegmentId,
) -> Result<Vec<LogEntry>, StoreError> {
self.inner.read_all(session_id, segment_id)
}
fn list_segments(&self) -> Result<Vec<SegmentId>, StoreError> {
self.inner.list_segments()
fn list_sessions(&self) -> Result<Vec<SessionId>, StoreError> {
self.inner.list_sessions()
}
fn create_segment(&self, id: SegmentId, entries: &[LogEntry]) -> Result<(), StoreError> {
self.inner.create_segment(id, entries)
fn list_segments(&self, session_id: SessionId) -> Result<Vec<SegmentId>, StoreError> {
self.inner.list_segments(session_id)
}
fn exists(&self, id: SegmentId) -> Result<bool, StoreError> {
self.inner.exists(id)
fn lookup_session_of(&self, segment_id: SegmentId) -> Result<Option<SessionId>, StoreError> {
self.inner.lookup_session_of(segment_id)
}
fn read_entry_count(&self, id: SegmentId) -> Result<usize, StoreError> {
self.inner.read_entry_count(id)
fn create_segment(
&self,
session_id: SessionId,
segment_id: SegmentId,
entries: &[LogEntry],
) -> Result<(), StoreError> {
self.inner.create_segment(session_id, segment_id, entries)
}
fn append_trace(&self, id: SegmentId, entry: &TraceEntry) -> Result<(), StoreError> {
self.inner.append_trace(id, entry)
fn exists(&self, session_id: SessionId, segment_id: SegmentId) -> Result<bool, StoreError> {
self.inner.exists(session_id, segment_id)
}
fn read_entry_count(
&self,
session_id: SessionId,
segment_id: SegmentId,
) -> Result<usize, StoreError> {
self.inner.read_entry_count(session_id, segment_id)
}
fn append_trace(
&self,
session_id: SessionId,
segment_id: SegmentId,
entry: &TraceEntry,
) -> Result<(), StoreError> {
self.inner.append_trace(session_id, segment_id, entry)
}
}
@@ -386,12 +417,13 @@ async fn metric_write_failure_emits_warn_alert_and_does_not_abort_run() {
let alerter = pod::Alerter::new(tx);
pod.attach_alerter(alerter);
let session_id = pod.session_id();
let segment_id = pod.segment_id();
// Run completes successfully despite metric failure.
pod.run_text("hello").await.unwrap();
// No metrics ended up in the log (writes were rejected).
let state = session_store::restore(&store, segment_id).unwrap();
let state = session_store::restore(&store, session_id, segment_id).unwrap();
let metrics = metrics_from_extensions(&state.extensions);
assert!(metrics.is_empty(), "metrics must drop on write failure");
@@ -446,10 +478,11 @@ permission = "write"
let mut pod = Pod::new(manifest, worker, store.clone(), pwd, scope)
.await
.unwrap();
let session_id = pod.session_id();
let segment_id = pod.segment_id();
pod.run_text("hello").await.unwrap();
let state = session_store::restore(&store, segment_id).unwrap();
let state = session_store::restore(&store, session_id, segment_id).unwrap();
let metrics = metrics_from_extensions(&state.extensions);
assert!(
metrics.is_empty(),
@@ -182,7 +182,10 @@ async fn session_start_state_captures_rendered_prompt() {
.unwrap();
pod.run_text("hi").await.unwrap();
let entries = pod.store().read_all(pod.segment_id()).unwrap();
let entries = pod
.store()
.read_all(pod.session_id(), pod.segment_id())
.unwrap();
let first = entries.first().expect("at least one entry");
match first {
LogEntry::SegmentStart { system_prompt, .. } => {