update: 残存 Session 識別子の Segment 化(review follow-up)
レビュー指摘の通り、次の session-grouping-introduce で新 SessionId が
入る前に名称衝突を避けるため取り残しを掃除。
- PodError::Session{Empty,ScopeMissing} → Segment{Empty,ScopeMissing}
- ScopeLockError::SessionConflict → SegmentConflict
- Pod.session_state / SegmentState.set_session_id 系
- source_session_id / prev_session_id / ensure_session_head / short_session
- pod_cli の "Session ID:" 表示
- fs_store の sessions ローカル変数
This commit is contained in:
parent
7577577c9f
commit
d7ff25b6a7
|
|
@ -30,7 +30,7 @@ pub enum ScopeLockError {
|
|||
"session {segment_id} is already held by pod `{pod_name}` at {}",
|
||||
.socket.display()
|
||||
)]
|
||||
SessionConflict {
|
||||
SegmentConflict {
|
||||
segment_id: SegmentId,
|
||||
pod_name: String,
|
||||
socket: PathBuf,
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ pub fn update_segment(pod_name: &str, new_segment_id: SegmentId) -> Result<(), S
|
|||
let mut guard = LockFileGuard::open(&lock_path)?;
|
||||
if let Some(other) = guard.data().find_by_segment(new_segment_id) {
|
||||
if other.pod_name != pod_name {
|
||||
return Err(ScopeLockError::SessionConflict {
|
||||
return Err(ScopeLockError::SegmentConflict {
|
||||
segment_id: new_segment_id,
|
||||
pod_name: other.pod_name.clone(),
|
||||
socket: other.socket.clone(),
|
||||
|
|
@ -320,7 +320,7 @@ mod tests {
|
|||
// `a` cannot adopt b's live session id.
|
||||
let err = update_segment("a", s_b).unwrap_err();
|
||||
match err {
|
||||
ScopeLockError::SessionConflict {
|
||||
ScopeLockError::SegmentConflict {
|
||||
pod_name,
|
||||
segment_id,
|
||||
..
|
||||
|
|
@ -328,7 +328,7 @@ mod tests {
|
|||
assert_eq!(pod_name, "b");
|
||||
assert_eq!(segment_id, s_b);
|
||||
}
|
||||
other => panic!("expected SessionConflict, got {other:?}"),
|
||||
other => panic!("expected SegmentConflict, got {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ pub fn register_pod_with_deny(
|
|||
return Err(ScopeLockError::DuplicatePodName(pod_name));
|
||||
}
|
||||
if let Some(existing) = guard.data().find_by_segment(segment_id) {
|
||||
return Err(ScopeLockError::SessionConflict {
|
||||
return Err(ScopeLockError::SegmentConflict {
|
||||
segment_id,
|
||||
pod_name: existing.pod_name.clone(),
|
||||
socket: existing.socket.clone(),
|
||||
|
|
@ -588,7 +588,7 @@ mod tests {
|
|||
)
|
||||
.unwrap();
|
||||
// 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.
|
||||
let err = register_pod(
|
||||
&mut g,
|
||||
|
|
@ -600,7 +600,7 @@ mod tests {
|
|||
)
|
||||
.unwrap_err();
|
||||
match err {
|
||||
ScopeLockError::SessionConflict {
|
||||
ScopeLockError::SegmentConflict {
|
||||
segment_id,
|
||||
pod_name,
|
||||
..
|
||||
|
|
@ -608,7 +608,7 @@ mod tests {
|
|||
assert_eq!(segment_id, shared_session);
|
||||
assert_eq!(pod_name, "first");
|
||||
}
|
||||
other => panic!("expected SessionConflict, got {other:?}"),
|
||||
other => panic!("expected SegmentConflict, got {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
}
|
||||
|
||||
// 6. Session ID for potential restore
|
||||
println!("\nSession ID: {}", pod.segment_id());
|
||||
println!("\nSegment ID: {}", pod.segment_id());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -185,8 +185,8 @@ async fn main() -> ExitCode {
|
|||
return ExitCode::FAILURE;
|
||||
}
|
||||
}
|
||||
} else if let Some(source_session_id) = cli.session {
|
||||
match Pod::restore_from_manifest(source_session_id, manifest, store, loader).await {
|
||||
} else if let Some(source_segment_id) = cli.session {
|
||||
match Pod::restore_from_manifest(source_segment_id, manifest, store, loader).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("error: failed to restore pod: {e}");
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ use tokio::task::JoinHandle;
|
|||
/// 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_session_head`.
|
||||
/// concurrent writers in `ensure_segment_head`.
|
||||
pub struct SegmentState {
|
||||
segment_id: ArcSwap<SegmentId>,
|
||||
entries_written: AtomicUsize,
|
||||
|
|
@ -69,7 +69,7 @@ impl SegmentState {
|
|||
**self.segment_id.load()
|
||||
}
|
||||
|
||||
pub fn set_session_id(&self, id: SegmentId) {
|
||||
pub fn set_segment_id(&self, id: SegmentId) {
|
||||
self.segment_id.store(Arc::new(id));
|
||||
}
|
||||
|
||||
|
|
@ -163,8 +163,8 @@ pub struct Pod<C: LlmClient, St: Store> {
|
|||
store: St,
|
||||
/// Shared session pointer. Source of truth for the Pod's current
|
||||
/// `segment_id` and append tally. `self.segment_id()` is a thin
|
||||
/// wrapper over `session_state.segment_id()`.
|
||||
session_state: Arc<SegmentState>,
|
||||
/// wrapper over `segment_state.segment_id()`.
|
||||
segment_state: Arc<SegmentState>,
|
||||
/// Absolute working directory of the Pod.
|
||||
pwd: PathBuf,
|
||||
/// Shared, atomically-swappable view of the Pod's resolved scope.
|
||||
|
|
@ -338,7 +338,7 @@ impl<C: LlmClient + Clone + 'static, St: Store + Clone + 'static> Pod<C, St> {
|
|||
manifest: self.manifest.clone(),
|
||||
worker: Some(worker),
|
||||
store: self.store.clone(),
|
||||
session_state: self.session_state.clone(),
|
||||
segment_state: self.segment_state.clone(),
|
||||
pwd: self.pwd.clone(),
|
||||
scope: self.scope.clone(),
|
||||
hook_builder: HookRegistryBuilder::new(),
|
||||
|
|
@ -382,7 +382,7 @@ impl<C: LlmClient + Clone + 'static, St: Store + Clone + 'static> Pod<C, St> {
|
|||
pub fn log_writer_handle(&self) -> LogWriterHandle<St> {
|
||||
LogWriterHandle {
|
||||
store: self.store.clone(),
|
||||
state: self.session_state.clone(),
|
||||
state: self.segment_state.clone(),
|
||||
sink: self.sink.clone(),
|
||||
}
|
||||
}
|
||||
|
|
@ -469,7 +469,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
pwd: PathBuf,
|
||||
scope: Scope,
|
||||
) -> Result<Self, PodError> {
|
||||
// Segment 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
|
||||
// `set_system_prompt_template`) can be captured by `SegmentStart`.
|
||||
let segment_id = session_store::new_segment_id();
|
||||
|
|
@ -478,7 +478,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
manifest,
|
||||
worker: Some(worker),
|
||||
store,
|
||||
session_state: SegmentState::new(segment_id, 0),
|
||||
segment_state: SegmentState::new(segment_id, 0),
|
||||
pwd,
|
||||
scope: SharedScope::new(scope),
|
||||
hook_builder: HookRegistryBuilder::new(),
|
||||
|
|
@ -545,7 +545,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
/// The session ID used for persistence. Read lock-free from the
|
||||
/// shared session pointer so fork-time swaps are observed immediately.
|
||||
pub fn segment_id(&self) -> SegmentId {
|
||||
self.session_state.segment_id()
|
||||
self.segment_state.segment_id()
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// writes.
|
||||
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(());
|
||||
}
|
||||
let snapshot = {
|
||||
|
|
@ -627,9 +627,9 @@ 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.session_state.segment_id();
|
||||
let segment_id = self.segment_state.segment_id();
|
||||
self.store.append(segment_id, &entry)?;
|
||||
self.session_state.increment_entries();
|
||||
self.segment_state.increment_entries();
|
||||
self.sink.publish(entry);
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1143,7 +1143,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
self.ensure_interceptor_installed();
|
||||
self.ensure_system_prompt_materialized()?;
|
||||
self.cleanup_finished_memory_task();
|
||||
self.ensure_session_head()?;
|
||||
self.ensure_segment_head()?;
|
||||
if self.should_pre_run_compact() {
|
||||
self.join_memory_task().await;
|
||||
}
|
||||
|
|
@ -1616,10 +1616,10 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
/// `ensure_system_prompt_materialized` has just rendered. Subsequent
|
||||
/// calls fall through to entry-count comparison, which auto-forks
|
||||
/// 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 prev_session_id = self.session_state.segment_id();
|
||||
let entries_written = self.session_state.entries_written();
|
||||
let prev_segment_id = self.segment_state.segment_id();
|
||||
let entries_written = self.segment_state.entries_written();
|
||||
if entries_written == 0 {
|
||||
let initial = LogEntry::SegmentStart {
|
||||
ts: segment_log::now_millis(),
|
||||
|
|
@ -1636,7 +1636,7 @@ 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_session_id)
|
||||
.read_entry_count(prev_segment_id)
|
||||
.map_err(PodError::from)?;
|
||||
if store_count == entries_written {
|
||||
return Ok(());
|
||||
|
|
@ -1656,8 +1656,8 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
self.store
|
||||
.create_segment(fork_id, &[entry.clone()])
|
||||
.map_err(PodError::from)?;
|
||||
self.session_state.set_session_id(fork_id);
|
||||
self.session_state.set_entries_written(1);
|
||||
self.segment_state.set_segment_id(fork_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)?;
|
||||
|
|
@ -2145,7 +2145,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
// 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.session_state.segment_id();
|
||||
let old_session_id = self.segment_state.segment_id();
|
||||
let source_turn_count = self.worker.as_ref().unwrap().turn_count();
|
||||
let w = self.worker.as_ref().unwrap();
|
||||
let entry = LogEntry::SegmentStart {
|
||||
|
|
@ -2160,8 +2160,8 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
}),
|
||||
};
|
||||
self.store.create_segment(new_segment_id, &[entry.clone()])?;
|
||||
self.session_state.set_session_id(new_segment_id);
|
||||
self.session_state.set_entries_written(1);
|
||||
self.segment_state.set_segment_id(new_segment_id);
|
||||
self.segment_state.set_entries_written(1);
|
||||
let session_start = entry;
|
||||
// Broadcast the SegmentStart through the sink. This atomically
|
||||
// resets the mirror to `[SegmentStart]` so any subscriber
|
||||
|
|
@ -2435,12 +2435,12 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
extract::ExtractedPayload::default()
|
||||
});
|
||||
|
||||
let source_session_id = self.session_state.segment_id();
|
||||
let source_segment_id = self.segment_state.segment_id();
|
||||
let staging_id = if payload.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
let source = memory::schema::SourceRef {
|
||||
segment_id: source_session_id.to_string(),
|
||||
segment_id: source_segment_id.to_string(),
|
||||
range: [start_entry as u64, end_entry as u64],
|
||||
};
|
||||
let (id, _) = extract::write_staging(&layout, source, payload)
|
||||
|
|
@ -2736,7 +2736,7 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
|
|||
let skill_shadows = std::mem::take(&mut common.skill_shadows);
|
||||
|
||||
// Segment creation is deferred to the first run (see
|
||||
// `ensure_session_head`) so the SegmentStart entry can capture
|
||||
// `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.
|
||||
|
|
@ -2765,7 +2765,7 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
|
|||
manifest,
|
||||
worker: Some(worker),
|
||||
store,
|
||||
session_state: SegmentState::new(segment_id, 0),
|
||||
segment_state: SegmentState::new(segment_id, 0),
|
||||
pwd: common.pwd,
|
||||
scope: SharedScope::new(common.scope),
|
||||
hook_builder: HookRegistryBuilder::new(),
|
||||
|
|
@ -2835,7 +2835,7 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
|
|||
manifest,
|
||||
worker: Some(worker),
|
||||
store,
|
||||
session_state: SegmentState::new(segment_id, 0),
|
||||
segment_state: SegmentState::new(segment_id, 0),
|
||||
pwd: common.pwd,
|
||||
scope: SharedScope::new(common.scope),
|
||||
hook_builder: HookRegistryBuilder::new(),
|
||||
|
|
@ -2903,13 +2903,13 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
|
|||
let raw_entries = store.read_all(segment_id)?;
|
||||
let state = session_store::collect_state(&raw_entries);
|
||||
if state.entries_count == 0 {
|
||||
return Err(PodError::SessionEmpty { segment_id });
|
||||
return Err(PodError::SegmentEmpty { segment_id });
|
||||
}
|
||||
let mirror_entries: Vec<LogEntry> = raw_entries.clone();
|
||||
let scope_snapshot = state
|
||||
.pod_scope
|
||||
.clone()
|
||||
.ok_or(PodError::SessionScopeMissing { segment_id })?;
|
||||
.ok_or(PodError::SegmentScopeMissing { segment_id })?;
|
||||
|
||||
let mut common = prepare_pod_common_with_scope(
|
||||
&manifest,
|
||||
|
|
@ -2974,7 +2974,7 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
|
|||
manifest,
|
||||
worker: Some(worker),
|
||||
store,
|
||||
session_state: SegmentState::new(segment_id, state.entries_count),
|
||||
segment_state: SegmentState::new(segment_id, state.entries_count),
|
||||
pwd: common.pwd,
|
||||
scope: SharedScope::new(common.scope),
|
||||
hook_builder: HookRegistryBuilder::new(),
|
||||
|
|
@ -3235,12 +3235,12 @@ pub enum PodError {
|
|||
WorkflowResolve(#[from] WorkflowResolveError),
|
||||
|
||||
#[error("session {segment_id} has no entries to restore")]
|
||||
SessionEmpty { segment_id: SegmentId },
|
||||
SegmentEmpty { segment_id: SegmentId },
|
||||
|
||||
#[error(
|
||||
"session {segment_id} has no persisted scope snapshot; refusing resume without explicit scope"
|
||||
)]
|
||||
SessionScopeMissing { segment_id: SegmentId },
|
||||
SegmentScopeMissing { segment_id: SegmentId },
|
||||
}
|
||||
|
||||
/// Bundle of resources that every high-level Pod constructor needs:
|
||||
|
|
|
|||
|
|
@ -482,7 +482,7 @@ fn pod_registry_err_to_tool(e: ScopeLockError) -> ToolError {
|
|||
| ScopeLockError::WriteConflict { .. }
|
||||
| ScopeLockError::DuplicatePodName(_)
|
||||
| ScopeLockError::UnknownPod(_)
|
||||
| ScopeLockError::SessionConflict { .. } => ToolError::InvalidArgument(e.to_string()),
|
||||
| ScopeLockError::SegmentConflict { .. } => ToolError::InvalidArgument(e.to_string()),
|
||||
ScopeLockError::Io(_) => ToolError::ExecutionFailed(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ async fn restore_from_manifest_rejects_empty_session_log() {
|
|||
|
||||
// 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 `SessionEmpty` *before* it
|
||||
// `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();
|
||||
|
|
@ -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;
|
||||
|
||||
match result {
|
||||
Err(PodError::SessionEmpty { segment_id }) => assert_eq!(segment_id, id),
|
||||
Err(other) => panic!("expected SessionEmpty, got {other:?}"),
|
||||
Err(PodError::SegmentEmpty { segment_id }) => assert_eq!(segment_id, id),
|
||||
Err(other) => panic!("expected SegmentEmpty, got {other:?}"),
|
||||
Ok(_) => panic!("expected empty session log to fail"),
|
||||
}
|
||||
}
|
||||
|
|
@ -101,8 +101,8 @@ async fn restore_from_manifest_rejects_session_without_scope_snapshot() {
|
|||
Pod::restore_from_manifest(id, manifest, store, pod::PromptLoader::builtins_only()).await;
|
||||
|
||||
match result {
|
||||
Err(PodError::SessionScopeMissing { segment_id }) => assert_eq!(segment_id, id),
|
||||
Err(other) => panic!("expected SessionScopeMissing, got {other:?}"),
|
||||
Err(PodError::SegmentScopeMissing { segment_id }) => assert_eq!(segment_id, id),
|
||||
Err(other) => panic!("expected SegmentScopeMissing, got {other:?}"),
|
||||
Ok(_) => panic!("expected missing scope snapshot to fail"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ impl Store for FsStore {
|
|||
}
|
||||
|
||||
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)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
|
@ -89,13 +89,13 @@ impl Store for FsStore {
|
|||
if name.ends_with(".jsonl") && !name.ends_with(".trace.jsonl") {
|
||||
let stem = name.trim_end_matches(".jsonl");
|
||||
if let Ok(id) = stem.parse::<SegmentId>() {
|
||||
sessions.push(id);
|
||||
segments.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
// UUID v7: lexicographic sort = chronological sort, newest first
|
||||
sessions.sort_by(|a, b| b.cmp(a));
|
||||
Ok(sessions)
|
||||
segments.sort_by(|a, b| b.cmp(a));
|
||||
Ok(segments)
|
||||
}
|
||||
|
||||
fn create_segment(&self, id: SegmentId, entries: &[LogEntry]) -> Result<(), StoreError> {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ pub fn create_segment_with_id(
|
|||
pub fn create_compacted_segment(
|
||||
store: &impl Store,
|
||||
state: SegmentStartState<'_>,
|
||||
source_session_id: SegmentId,
|
||||
source_segment_id: SegmentId,
|
||||
source_turn_count: usize,
|
||||
) -> Result<SegmentId, StoreError> {
|
||||
let segment_id = crate::new_segment_id();
|
||||
|
|
@ -71,7 +71,7 @@ pub fn create_compacted_segment(
|
|||
history: to_logged(state.history),
|
||||
forked_from: None,
|
||||
compacted_from: Some(SegmentOrigin {
|
||||
segment_id: source_session_id,
|
||||
segment_id: source_segment_id,
|
||||
at_turn_index: source_turn_count,
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ struct Row {
|
|||
preview: String,
|
||||
/// `Some(pod_name)` when a live Pod currently holds an allocation
|
||||
/// 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.
|
||||
live_pod: Option<String>,
|
||||
}
|
||||
|
|
@ -300,7 +300,7 @@ fn row_line(row: &Row, selected: bool) -> Line<'_> {
|
|||
};
|
||||
let mut spans = vec![
|
||||
Span::raw(marker),
|
||||
Span::styled(short_session(row.id), id_style),
|
||||
Span::styled(short_segment(row.id), id_style),
|
||||
Span::raw(" "),
|
||||
];
|
||||
if let Some(ref pod_name) = row.live_pod {
|
||||
|
|
@ -313,7 +313,7 @@ fn row_line(row: &Row, selected: bool) -> Line<'_> {
|
|||
Line::from(spans)
|
||||
}
|
||||
|
||||
fn short_session(id: SegmentId) -> String {
|
||||
fn short_segment(id: SegmentId) -> String {
|
||||
let s = id.to_string();
|
||||
s.chars().take(8).collect()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -445,7 +445,7 @@ fn draw_form(f: &mut Frame<'_>, form: &Form) {
|
|||
.split(area);
|
||||
|
||||
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(),
|
||||
};
|
||||
let title = Paragraph::new(Line::from(vec
|
||||
- 日付: 2026-05-20
|
||||
|
|
|
|||
69
tickets/segment-rename.review.md
Normal file
69
tickets/segment-rename.review.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# Review: session-store: SessionId → SegmentId へのリネーム
|
||||
|
||||
対象コミット:
|
||||
- `0d7c37f update: SessionId / SessionStart / SessionOrigin 等を Segment 系名称へ`
|
||||
- `7577577 update: Session-lifetime/scoped を Pod-lifetime に修正`
|
||||
|
||||
ベース: `develop` (`d5c7330 Merge: entry-hash-abolish`)
|
||||
|
||||
## 前提・要件の確認
|
||||
|
||||
- **`SessionId` 型がコードベースに残らない**: 達成。`grep -rn 'SessionId' crates --include='*.rs'` でヒットゼロ。`SegmentId` 型 alias は `crates/session-store/src/lib.rs:56` に定義され、`new_segment_id()` (同 lib.rs:59) も改名済み。
|
||||
- **`SessionStart` / `SessionOrigin` / `SessionState` / `SessionLogSink` / `SessionLockInfo` 等の segment レベル `Session*` 型を Segment に統一**: 達成。同 grep でこれらの PascalCase 識別子も全て不在。`SegmentStart{,State}` (`session-store/src/segment.rs:18`)、`SegmentOrigin` (`segment_log.rs:175`)、`SegmentState` (`pod/src/pod.rs:60`)、`SegmentLogSink` (`pod/src/segment_log_sink.rs:42`)、`SegmentLockInfo` (pod-registry) に置き換わっている。
|
||||
- **`Store::{list_segments, create_segment}` / `create_compacted_segment` / `pod_registry::{update_segment, lookup_segment, find_by_segment}` への改名**: 達成。`session-store/src/store.rs:48,51` で trait 名前が更新され、`pod-registry/src/{lifecycle,mutate,table}.rs` の呼び出し元と本体も全て追従。
|
||||
- **`protocol::Event::SegmentRotated` / `CompactDone.new_segment_id`**: 達成。`crates/protocol/src/lib.rs:383, 411`。`SessionRotated` の grep もヒットゼロ。
|
||||
- **module / file rename**: `crates/session-store/src/{session.rs → segment.rs}`、`{session_log.rs → segment_log.rs}`、`crates/pod/src/{session_log_sink.rs → segment_log_sink.rs}` が `git mv` で実行され、`pub mod` / `use` 宣言も追従。
|
||||
- **crate 名 `session-store` の保留**: 維持。`Cargo.toml` の `name = "session-store"` はそのまま。
|
||||
- **CLI flag `--session` / TUI 内部 `ResumeWithSession` / `InvalidSession` / `~/.insomnia/sessions/` ディレクトリ**: 維持。`crates/tui/src/main.rs:54-99`、`crates/pod/src/main.rs:188`、`manifest::paths::sessions_dir()` 経路は触られていない。defer 方針通り。
|
||||
- **`cargo check --workspace` と全テスト pass**: 確認。`target/debug` で `Finished dev` まで通り、`cargo test --workspace` も `test result: ok` のみで完走(`double_run_returns_error` の flakiness はチケット冒頭で報告済みの KNOWN_ISSUES 既知別件)。
|
||||
- **既存 JSONL を Segment 中心 API で読み書きできる**: `LogEntry::SegmentStart` で serde 表記が新しく `kind = "segment_start"` 系に倒れる方向 (Rust の rename 規約に基づく)。旧 JSONL を読みたい場合は migration が要るが、CLAUDE.md 方針「不必要な後方互換は作らない」と整合。新コードで作って読む限りは round-trip する (`session-store/tests/fs_store_test.rs`, `session_test.rs` の全 pass で確認)。
|
||||
|
||||
## アーキテクチャ・スコープ
|
||||
|
||||
- 機械的 rename のみで、層境界・API 構造に変化なし。llm-worker / pod / pod-registry / protocol / tui の全層で同じ語彙が一斉に移っているので、コードベースの語彙整合性は素直に上がっている。
|
||||
- 第 2 コミット (`7577577`) の `Session-lifetime` → `Pod-lifetime` 是正は、ticket の rename 趣旨を越えるが、関連語彙の整理として妥当。`tools::TaskStore` / `tools::Tracker` は compaction を跨いで `Pod` プロセス寿命まで生きるという事実関係に doc が即している。むしろ「旧 SessionId=Segment 時代でも `Session-scoped` 表現は不正確だった」点を解消できているので、本チケットの中で片付けた判断は妥当。
|
||||
- file rename を要求していない要件に対して、`session.rs → segment.rs` 等の module rename まで踏み込んだ判断は、対称性確保として妥当。`pub mod` / `pub use` の更新コストは小さく、ファイル名と中身の概念が乖離する状態を避ける方が長期保守に有利。過剰ではない。
|
||||
- 次チケット `session-grouping-introduce` で新たに `SessionId` 型 (Segment 群の grouping) が入る予定なので、現状の rename は新型導入と語彙衝突しない土台を提供できている。
|
||||
|
||||
## 指摘事項
|
||||
|
||||
### Blocking
|
||||
- なし。
|
||||
|
||||
### Non-blocking / Follow-up
|
||||
|
||||
- **公開エラー variant が `Session*` のまま残っている**: 以下は segment レベルの条件で発火するため、ticket の rename 方針 (segment レベルを指す Session* 型は Segment に倒す) に従って後続で追従するのが望ましい。次チケットで新 `SessionId` が入ると、これらの variant 名は誤読を招く。
|
||||
- `PodError::SessionEmpty { segment_id: SegmentId }` (`crates/pod/src/pod.rs:3238`) → `SegmentEmpty`
|
||||
- `PodError::SessionScopeMissing { segment_id: SegmentId }` (`crates/pod/src/pod.rs:3243`) → `SegmentScopeMissing`
|
||||
- `ScopeLockError::SessionConflict { segment_id: SegmentId, ... }` (`crates/pod-registry/src/error.rs:33`) → `SegmentConflict`
|
||||
- 対応する `#[error("session {segment_id} ...")]` メッセージ文字列、`tui/src/spawn.rs:60` の重複文言、テスト (`pod/tests/restore_test.rs:78,79,104,105`, `pod-registry/src/{lifecycle.rs:323-331, mutate.rs:591,603,611}`) も連動。
|
||||
- **`SegmentState` の setter 名が `set_session_id`**: `crates/pod/src/pod.rs:72`。型 (`SegmentState`) と getter (`segment_id()`) は改名済みだが setter だけ旧名のまま。`set_segment_id` に揃えるのが自然 (呼び出しは `pod.rs:1659, 2163` の 2 箇所のみ)。
|
||||
- **`Pod` 構造体のフィールド `session_state: Arc<SegmentState>`**: `crates/pod/src/pod.rs:167` および計 12 箇所 (340, 385, 481, 548, 607, 630, 632, 1621, 1659, 1660, 2148, 2163, 2438, 2768, 2838, 2977)。型は `SegmentState` なのにフィールド名が旧概念に固定されている。`segment_state` に揃えるのが整合的。
|
||||
- **`pod_cli.rs:78-79` の取り残し**: 第 2 コミットで line 57 を `Segment:` に直したが、末尾の `println!("\nSession ID: {}", pod.segment_id())` は据え置き。ユーザー向けサンプルなので意図的なら問題ないが、第 2 コミットの趣旨 (`Session:` → `Segment:`) と非対称。
|
||||
- **doc comment の旧語彙残り (narrative)**:
|
||||
- `crates/session-store/src/lib.rs:5` "Sessions are recorded as a sequence of `LogEntry`..." (本文は Segment が主語のはず)
|
||||
- `crates/session-store/src/segment_log.rs:154-159` "session-store は payload を不透明扱い... 「session 寿命に縛りたいが session-store の型を汚したくない」"
|
||||
- `crates/pod/src/segment_log_sink.rs:1,36,47,51,107,142,158,170,181` "session-log mirror" / "session log mirror mutex poisoned" 等
|
||||
- `crates/pod/src/pod.rs:157,164,166` "persists session state via `session-store`" / "Shared session pointer"
|
||||
- これらは旧チケットの慣性で残っているもの。即時の修正は必須でないが、`session-grouping-introduce` で本物の Session 概念が入ると曖昧度が増す。
|
||||
|
||||
### Nits
|
||||
|
||||
- **テスト関数名と test ファイル名の取り残し**:
|
||||
- `crates/session-store/tests/session_test.rs` (ファイル名)。中身は segment 操作のテスト群 (`session_run_*`, `session_restore_round_trip` 等)。次の grouping 導入で本物の session レベルのテストを足す場合に名前が衝突する。
|
||||
- `crates/session-store/tests/fs_store_test.rs:56,82,135` (`create_session_writes_all_entries`, `list_sessions_returns_newest_first`, `not_found_error_for_missing_session`)。
|
||||
- `crates/pod-registry/src/lifecycle.rs:258,279,299,320` (`lookup_session_returns_live_writer_info`, `update_session_rewrites_allocation_session_id`, `update_session_rejects_when_target_already_held`)。
|
||||
- `crates/pod-registry/src/mutate.rs:576` (`register_pod_rejects_session_id_collision`)。
|
||||
- `crates/pod-registry/src/table.rs:224` (`find_by_session_skips_none_placeholders`)。
|
||||
- `crates/pod/tests/restore_test.rs:35,58,85` (`restore_from_manifest_rejects_unknown_session`, `_rejects_empty_session_log`, `_rejects_session_without_scope_snapshot`)。
|
||||
- `crates/pod/tests/compact_events_test.rs:182,217,572` (`system_texts_in_sink_session_start`, `compact_emits_session_start_carrying_summary_and_task_snapshot`, `detached_extract_does_not_fork_session_log`)。
|
||||
- `crates/pod/tests/system_prompt_template_test.rs:178` (`session_start_state_captures_rendered_prompt`)。
|
||||
- `crates/pod/src/segment_log_sink.rs:203` (test helper fn `session_start()`)。
|
||||
- **ローカル変数名の取り残し**: `source_session_id` (`pod.rs:2148, 2438`, `session-store/src/segment.rs:63,74`)、`prev_session_id` (`pod.rs:1621`)、`old_session_id` (`pod.rs:2148`)、`session_id_for_usage` (`controller.rs:433`)、`usage_session_id` (`memory/src/tool/read.rs:42`)、`sessions` (`session-store/src/fs_store.rs:83-98`)。`ensure_session_head` 関数名 (`pod.rs:1619`) も同様。
|
||||
- **`tui/src/picker.rs:316`, `tui/src/spawn.rs:476`**: `short_session(id: SegmentId)` 関数。短縮表示の対象は SegmentId だが関数名は旧名のまま。CLI flag `--session` 側のユーザー文脈に寄せているなら据え置きでも可。
|
||||
|
||||
## 判断
|
||||
|
||||
**Approve with follow-up** — 要件 (型 rename 網羅 / build & test pass / defer 対象は据え置き) は全て満たされ、`SessionId` 識別子の根絶も完遂。第 2 コミットの `Pod-lifetime` 是正も無関係な改善ではなく、ticket 趣旨の周辺整理として妥当。
|
||||
|
||||
ただし `PodError::SessionEmpty` / `ScopeLockError::SessionConflict` 等の **public な error variant 名** と、`Pod::session_state` フィールド・`SegmentState::set_session_id` メソッドの取り残しは、次チケット `session-grouping-introduce` で新 `SessionId` 型が導入される前に片付けておくのが理想 (放置すると新型と命名衝突して読みにくくなる)。
|
||||
Loading…
Reference in New Issue
Block a user