From bc21c94618977702156ce4d684db6a3965ff2c6d Mon Sep 17 00:00:00 2001 From: daniel Date: Sun, 30 Aug 2026 23:29:27 +0100 Subject: [PATCH 1/2] fix(sessions): hide subagent sessions from catalogs --- docs/user/tui-and-sessions.md | 2 +- src/runtime.rs | 12 ++++++- src/runtime/tests.rs | 23 +++++++++++++ src/session.rs | 63 +++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 2 deletions(-) diff --git a/docs/user/tui-and-sessions.md b/docs/user/tui-and-sessions.md index 0e0cfe2..24e6668 100644 --- a/docs/user/tui-and-sessions.md +++ b/docs/user/tui-and-sessions.md @@ -17,7 +17,7 @@ kit sessions --root /path/to/project kit tui --root /path/to/project --resume ``` -The catalog requires an existing directory and is workspace-filtered and newest-first. It reports each durable ID and updated time. The title comes from the earliest retained useful user text so compaction does not rename a session; the preview describes the current retained history. Display metadata removes terminal controls and Unicode default-ignorable formatting characters. +The catalog requires an existing directory and is workspace-filtered and newest-first. It reports each durable top-level session ID and updated time; sessions created as subagents are omitted based on structured origin metadata in their initial transcript. Sessions created before Kit recorded that metadata remain visible. Filtering affects discovery only; a known omitted ID can still be resumed explicitly. The title comes from the earliest retained useful user text so compaction does not rename a session; the preview describes the current retained history. Display metadata removes terminal controls and Unicode default-ignorable formatting characters. A session ID must be 1–128 ASCII letters, digits, `-`, or `_`. `kit prompt` uses the same durable sessions: it prints `session_id: ` after its answer, and that ID can be continued by either `kit prompt --resume ` or `kit tui --resume `. diff --git a/src/runtime.rs b/src/runtime.rs index 30907ec..3c4ee57 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -1096,7 +1096,17 @@ impl Runtime { } async fn initial_transcript(&self, depth: usize) -> Result, String> { - load_initial_transcript(&self.root, self.system_prompt(depth)).await + let mut transcript = load_initial_transcript(&self.root, self.system_prompt(depth)).await?; + let origin = if depth > 0 { + crate::session::SUBAGENT_SESSION_ORIGIN + } else { + crate::session::TOP_LEVEL_SESSION_ORIGIN + }; + transcript[0].metadata.insert( + crate::session::SESSION_ORIGIN_METADATA_KEY.into(), + Value::String(origin.into()), + ); + Ok(transcript) } fn system_prompt(&self, depth: usize) -> String { diff --git a/src/runtime/tests.rs b/src/runtime/tests.rs index 5d58915..db0d594 100644 --- a/src/runtime/tests.rs +++ b/src/runtime/tests.rs @@ -1123,6 +1123,29 @@ fn cancel_all_covers_running_and_late_background_registration() { jobs.finish_for_test("next-turn"); } +#[tokio::test] +async fn initial_transcript_records_structured_session_origin() { + let root = tempfile::tempdir().unwrap(); + let runtime = Runtime::new(root.path(), "gpt-5.4").unwrap(); + + for (depth, expected) in [ + (0, crate::session::TOP_LEVEL_SESSION_ORIGIN), + (1, crate::session::SUBAGENT_SESSION_ORIGIN), + ( + runtime.max_subagent_depth(), + crate::session::SUBAGENT_SESSION_ORIGIN, + ), + ] { + let transcript = runtime.initial_transcript(depth).await.unwrap(); + assert_eq!( + transcript[0] + .metadata + .get(crate::session::SESSION_ORIGIN_METADATA_KEY), + Some(&Value::String(expected.into())) + ); + } +} + #[test] fn system_prompt_guides_compose_and_subagent_hygiene() { let root = tempfile::tempdir().unwrap(); diff --git a/src/session.rs b/src/session.rs index 297a6bc..4cdbb8b 100644 --- a/src/session.rs +++ b/src/session.rs @@ -21,6 +21,9 @@ const REDIRECT_SCHEMA_VERSION: u32 = 4; const PREVIOUS_SCHEMA_VERSION: u32 = 2; const LEGACY_SCHEMA_VERSION: u32 = 1; const MAX_RFC3339_MILLIS: u64 = 253_402_300_799_999; +pub(crate) const SESSION_ORIGIN_METADATA_KEY: &str = "dev.kit.session.origin"; +pub(crate) const SUBAGENT_SESSION_ORIGIN: &str = "subagent"; +pub(crate) const TOP_LEVEL_SESSION_ORIGIN: &str = "top_level"; static NEXT_ID: AtomicU64 = AtomicU64::new(1); #[derive(Debug, Serialize, Deserialize)] @@ -847,6 +850,9 @@ fn catalog_for_workspace( let Ok(Some(authority)) = select_authority_with(global_directory, &root, &id, false) else { continue; }; + if catalog_is_subagent(&authority.historical_items, &authority.items) { + continue; + } let (title, preview) = catalog_text(&authority.historical_items, &authority.items); let item_updated = authority .items @@ -880,6 +886,22 @@ fn catalog_for_workspace( Ok(entries) } +fn catalog_is_subagent(historical_items: &[Vec], current_items: &[Item]) -> bool { + historical_items + .iter() + .map(Vec::as_slice) + .chain(std::iter::once(current_items)) + .flatten() + .any(|item| { + item.kind == ItemKind::System + && item + .metadata + .get(SESSION_ORIGIN_METADATA_KEY) + .and_then(serde_json::Value::as_str) + == Some(SUBAGENT_SESSION_ORIGIN) + }) +} + fn catalog_text( historical_items: &[Vec], current_items: &[Item], @@ -2722,6 +2744,47 @@ mod tests { drop((older, newer, other)); } + #[test] + fn catalog_omits_only_structurally_marked_subagent_sessions() { + let storage = tempfile::tempdir().unwrap(); + let root = tempfile::tempdir().unwrap(); + + let open_with_origin = |id: &str, origin: Option| { + let mut item = Item::text(ItemKind::System, format!("{id} system prompt")); + if let Some(origin) = origin { + item.metadata + .insert(SESSION_ORIGIN_METADATA_KEY.into(), origin); + } + open_in(root.path(), storage.path(), id, false, false, vec![item]).unwrap() + }; + let legacy = open_with_origin("legacy", None); + let top_level = open_with_origin( + "top-level", + Some(serde_json::Value::String(TOP_LEVEL_SESSION_ORIGIN.into())), + ); + let malformed = open_with_origin("malformed", Some(serde_json::Value::Bool(true))); + let subagent = open_with_origin( + "subagent", + Some(serde_json::Value::String(SUBAGENT_SESSION_ORIGIN.into())), + ); + subagent + .observer + .replace(&[Item::text(ItemKind::System, "compacted system prompt")]) + .unwrap(); + + let entries = catalog_for_workspace(root.path(), storage.path()).unwrap(); + let ids = entries + .iter() + .map(|entry| entry.id.as_str()) + .collect::>(); + assert_eq!(ids.len(), 3); + assert!(ids.contains(&"legacy")); + assert!(ids.contains(&"top-level")); + assert!(ids.contains(&"malformed")); + assert!(!ids.contains(&"subagent")); + drop((legacy, top_level, malformed, subagent)); + } + #[test] fn catalog_timestamps_are_rfc3339() { assert_eq!(timestamp_rfc3339(0), "1970-01-01T00:00:00.000Z"); From 1f4bfab3b7bf166eb82944fedfe47738ac345ccb Mon Sep 17 00:00:00 2001 From: daniel Date: Mon, 31 Aug 2026 10:56:04 +0100 Subject: [PATCH 2/2] chore: version bump --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 092b3cd..e4362c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2554,7 +2554,7 @@ dependencies = [ [[package]] name = "kit" -version = "0.1.116" +version = "0.1.117" dependencies = [ "a2a-protocol-client", "a2a-protocol-server", diff --git a/Cargo.toml b/Cargo.toml index 5616487..91e758e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kit" -version = "0.1.116" +version = "0.1.117" edition = "2024" rust-version = "1.94.0" publish = false