update: session-grouping review follow-up

- PickerOutcome::Picked から未使用の session_id を除去(pod-cli が lookup_session_of で再解決)
- picker preview が singular AssistantItem も拾うように
- fs_store layout doc に migration(後方互換なし、旧 flat sessions は破棄)を明記
- TaskStore は Session-lifetime、ScopedFs/Tracker は Pod-process lifetime と用語整理
- Pod::session_id / from_manifest_spawned のコメント補強
This commit is contained in:
2026-05-20 06:29:37 +09:00
parent 5edc4d3b03
commit 0dfdd11921
9 changed files with 155 additions and 26 deletions
+5 -1
View File
@@ -570,7 +570,9 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
}
/// The Session this Pod belongs to. Stable across compaction and
/// in-Session fork — only `fork` (a brand-new Session) changes it.
/// auto-fork (both stay within the same Session); there is no
/// Pod-level operation today that moves a running Pod to a different
/// Session.
pub fn session_id(&self) -> SessionId {
self.segment_state.session_id()
}
@@ -2864,6 +2866,8 @@ 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);
// A spawned child starts its own conversation, so it mints a
// fresh Session rather than joining the spawner's.
let session_id = session_store::new_session_id();
let segment_id = session_store::new_segment_id();
let scope_allocation = pod_registry::adopt_allocation(
+8
View File
@@ -6,6 +6,14 @@
//!
//! The per-Session directory makes `list_segments(session_id)` an O(dir)
//! scan and gives the fork tree a visible grouping in the filesystem.
//!
//! Migration: this layout is incompatible with the pre-`session-grouping`
//! flat `{root}/{segment_id}.jsonl` form. Project policy is no
//! backward compatibility — discard `~/.insomnia/sessions/` (or whatever
//! `root` resolved to) before running the new code. `list_sessions`
//! ignores top-level files outside session directories, so leftover
//! flat files do not corrupt new sessions, but they are no longer
//! enumerable by the picker.
use crate::event_trace::TraceEntry;
use crate::segment_log::LogEntry;
+7 -7
View File
@@ -4,12 +4,12 @@
//! `llm-worker` `Tool` infrastructure. Filesystem access is mediated by
//! two orthogonal concerns:
//!
//! - [`ScopedFs`] — pod-lifetime, expresses the write-block boundary for
//! the current scope. Derived from the manifest and shareable across
//! sessions.
//! - [`Tracker`] — session-lifetime, enforces the "read before edit"
//! - [`ScopedFs`] — Pod-process lifetime, expresses the write-block
//! boundary for the current scope. Derived from the manifest; not
//! persisted across Pod restart.
//! - [`Tracker`] — Pod-process lifetime, enforces the "read before edit"
//! policy via content hashes and tracks the recency of touched files.
//! Recreated fresh per session.
//! Recreated fresh on each Pod start (including resume).
//!
//! The Pod layer owns both instances and passes them to
//! [`builtin_tools`] when registering tools on a `Worker`.
@@ -42,11 +42,11 @@ pub use tracker::Tracker;
pub use write::write_tool;
/// Register all builtin tools, wiring them to a shared `ScopedFs`
/// (pod-lifetime) and `Tracker` (session-lifetime).
/// (Pod-process lifetime) and `Tracker` (Pod-process lifetime).
///
/// All returned factories share the same tracker instance so that
/// `Read` / `Write` / `Edit` see a consistent history across tool
/// invocations within a single session.
/// invocations within a single Pod run.
///
/// `bash_output_dir` is where the Bash tool spills long outputs. The
/// caller is responsible for adding that path to the readable scope
+5 -4
View File
@@ -1,8 +1,9 @@
//! Pod-lifetime TaskStore and builtin task tools.
//! Session-lifetime TaskStore and builtin task tools.
//!
//! The store is Pod/session-lifetime state shared by the four Task* tools. It
//! is reconstructed on resume by replaying TaskCreate / TaskUpdate tool-call
//! arguments from persisted history.
//! The store survives compaction and Pod restart — it is reconstructed
//! on resume by replaying TaskCreate / TaskUpdate tool-call arguments
//! from persisted history, so its effective lifetime is the
//! [`session_store::SessionId`] (the conversation), not the Pod process.
use std::sync::{Arc, Mutex};
+7 -5
View File
@@ -18,11 +18,13 @@
//!
//! # Lifetime
//!
//! A `Tracker` is **session-scoped**: the Pod layer creates a fresh
//! instance at the start of each agent session and discards it when the
//! session ends. The `ScopedFs` write boundary, by contrast, is
//! pod-lifetime (derived from the manifest). The two are orthogonal and
//! the Pod wires them together when registering builtin tools.
//! A `Tracker` is **Pod-process scoped**: the Pod layer creates a fresh
//! instance at the start of each Pod run (including resume) and discards
//! it when the process exits — it is not persisted, so a resumed
//! conversation starts with an empty read/edit history. The `ScopedFs`
//! write boundary is likewise Pod-process scoped (derived from the
//! manifest). The two are orthogonal and the Pod wires them together
//! when registering builtin tools.
//!
//! ```no_run
//! # use std::path::PathBuf;
+1 -1
View File
@@ -210,7 +210,7 @@ async fn run_resume() -> Result<(), Box<dyn std::error::Error>> {
// viewport before the name dialog opens so each phase gets fresh
// vertical room.
let leaf_segment_id = match picker::run().await? {
PickerOutcome::Picked { segment_id, .. } => segment_id,
PickerOutcome::Picked { segment_id } => segment_id,
PickerOutcome::Cancelled => return Ok(()),
};
run_spawn(Some(leaf_segment_id)).await
+10 -8
View File
@@ -62,11 +62,10 @@ impl From<session_store::StoreError> for PickerError {
}
pub enum PickerOutcome {
/// User picked a session; resume at its leaf segment.
Picked {
session_id: SessionId,
segment_id: SegmentId,
},
/// User picked a session; resume at its leaf segment. The pod-cli
/// rehydrates `session_id` via `Store::lookup_session_of` so we only
/// need to surface the segment here.
Picked { segment_id: SegmentId },
Cancelled,
}
@@ -134,10 +133,8 @@ pub async fn run() -> Result<PickerOutcome, PickerError> {
}
Some(Action::Submit) => {
close_viewport(&mut terminal)?;
let row = &rows[selected];
return Ok(PickerOutcome::Picked {
session_id: row.session_id,
segment_id: row.leaf_segment_id,
segment_id: rows[selected].leaf_segment_id,
});
}
Some(Action::Cancel) => {
@@ -200,6 +197,11 @@ fn last_message_preview(entries: &[LogEntry]) -> Option<String> {
return Some(format!("user: {}", trim_one_line(&text, 60)));
}
}
LogEntry::AssistantItem { item, .. } => {
if let Some(text) = first_text_logged(item) {
return Some(format!("assistant: {}", trim_one_line(&text, 60)));
}
}
LogEntry::AssistantItems { items, .. } => {
if let Some(text) = items.iter().find_map(first_text_logged) {
return Some(format!("assistant: {}", trim_one_line(&text, 60)));