Skip to content

Commit ec29b1e

Browse files
committed
fix(sessions): hide subagent sessions from catalogs
1 parent e9c159a commit ec29b1e

6 files changed

Lines changed: 100 additions & 4 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "kit"
3-
version = "0.1.115"
3+
version = "0.1.116"
44
edition = "2024"
55
rust-version = "1.94.0"
66
publish = false

docs/user/tui-and-sessions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ kit sessions --root /path/to/project
1717
kit tui --root /path/to/project --resume <session-id>
1818
```
1919

20-
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.
20+
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.
2121

2222
A session ID must be 1–128 ASCII letters, digits, `-`, or `_`. `kit prompt` uses the same durable sessions: it prints `session_id: <id>` after its answer, and that ID can be continued by either `kit prompt --resume <session-id>` or `kit tui --resume <session-id>`.
2323

src/runtime.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1096,7 +1096,17 @@ impl Runtime {
10961096
}
10971097

10981098
async fn initial_transcript(&self, depth: usize) -> Result<Vec<Item>, String> {
1099-
load_initial_transcript(&self.root, self.system_prompt(depth)).await
1099+
let mut transcript = load_initial_transcript(&self.root, self.system_prompt(depth)).await?;
1100+
let origin = if depth > 0 {
1101+
crate::session::SUBAGENT_SESSION_ORIGIN
1102+
} else {
1103+
crate::session::TOP_LEVEL_SESSION_ORIGIN
1104+
};
1105+
transcript[0].metadata.insert(
1106+
crate::session::SESSION_ORIGIN_METADATA_KEY.into(),
1107+
Value::String(origin.into()),
1108+
);
1109+
Ok(transcript)
11001110
}
11011111

11021112
fn system_prompt(&self, depth: usize) -> String {

src/runtime/tests.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,29 @@ fn cancel_all_covers_running_and_late_background_registration() {
11231123
jobs.finish_for_test("next-turn");
11241124
}
11251125

1126+
#[tokio::test]
1127+
async fn initial_transcript_records_structured_session_origin() {
1128+
let root = tempfile::tempdir().unwrap();
1129+
let runtime = Runtime::new(root.path(), "gpt-5.4").unwrap();
1130+
1131+
for (depth, expected) in [
1132+
(0, crate::session::TOP_LEVEL_SESSION_ORIGIN),
1133+
(1, crate::session::SUBAGENT_SESSION_ORIGIN),
1134+
(
1135+
runtime.max_subagent_depth(),
1136+
crate::session::SUBAGENT_SESSION_ORIGIN,
1137+
),
1138+
] {
1139+
let transcript = runtime.initial_transcript(depth).await.unwrap();
1140+
assert_eq!(
1141+
transcript[0]
1142+
.metadata
1143+
.get(crate::session::SESSION_ORIGIN_METADATA_KEY),
1144+
Some(&Value::String(expected.into()))
1145+
);
1146+
}
1147+
}
1148+
11261149
#[test]
11271150
fn system_prompt_guides_compose_and_subagent_hygiene() {
11281151
let root = tempfile::tempdir().unwrap();

src/session.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ const REDIRECT_SCHEMA_VERSION: u32 = 4;
2121
const PREVIOUS_SCHEMA_VERSION: u32 = 2;
2222
const LEGACY_SCHEMA_VERSION: u32 = 1;
2323
const MAX_RFC3339_MILLIS: u64 = 253_402_300_799_999;
24+
pub(crate) const SESSION_ORIGIN_METADATA_KEY: &str = "dev.kit.session.origin";
25+
pub(crate) const SUBAGENT_SESSION_ORIGIN: &str = "subagent";
26+
pub(crate) const TOP_LEVEL_SESSION_ORIGIN: &str = "top_level";
2427
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
2528

2629
#[derive(Debug, Serialize, Deserialize)]
@@ -847,6 +850,9 @@ fn catalog_for_workspace(
847850
let Ok(Some(authority)) = select_authority_with(global_directory, &root, &id, false) else {
848851
continue;
849852
};
853+
if catalog_is_subagent(&authority.historical_items, &authority.items) {
854+
continue;
855+
}
850856
let (title, preview) = catalog_text(&authority.historical_items, &authority.items);
851857
let item_updated = authority
852858
.items
@@ -880,6 +886,22 @@ fn catalog_for_workspace(
880886
Ok(entries)
881887
}
882888

889+
fn catalog_is_subagent(historical_items: &[Vec<Item>], current_items: &[Item]) -> bool {
890+
historical_items
891+
.iter()
892+
.map(Vec::as_slice)
893+
.chain(std::iter::once(current_items))
894+
.flatten()
895+
.any(|item| {
896+
item.kind == ItemKind::System
897+
&& item
898+
.metadata
899+
.get(SESSION_ORIGIN_METADATA_KEY)
900+
.and_then(serde_json::Value::as_str)
901+
== Some(SUBAGENT_SESSION_ORIGIN)
902+
})
903+
}
904+
883905
fn catalog_text(
884906
historical_items: &[Vec<Item>],
885907
current_items: &[Item],
@@ -2722,6 +2744,47 @@ mod tests {
27222744
drop((older, newer, other));
27232745
}
27242746

2747+
#[test]
2748+
fn catalog_omits_only_structurally_marked_subagent_sessions() {
2749+
let storage = tempfile::tempdir().unwrap();
2750+
let root = tempfile::tempdir().unwrap();
2751+
2752+
let open_with_origin = |id: &str, origin: Option<serde_json::Value>| {
2753+
let mut item = Item::text(ItemKind::System, format!("{id} system prompt"));
2754+
if let Some(origin) = origin {
2755+
item.metadata
2756+
.insert(SESSION_ORIGIN_METADATA_KEY.into(), origin);
2757+
}
2758+
open_in(root.path(), storage.path(), id, false, false, vec![item]).unwrap()
2759+
};
2760+
let legacy = open_with_origin("legacy", None);
2761+
let top_level = open_with_origin(
2762+
"top-level",
2763+
Some(serde_json::Value::String(TOP_LEVEL_SESSION_ORIGIN.into())),
2764+
);
2765+
let malformed = open_with_origin("malformed", Some(serde_json::Value::Bool(true)));
2766+
let subagent = open_with_origin(
2767+
"subagent",
2768+
Some(serde_json::Value::String(SUBAGENT_SESSION_ORIGIN.into())),
2769+
);
2770+
subagent
2771+
.observer
2772+
.replace(&[Item::text(ItemKind::System, "compacted system prompt")])
2773+
.unwrap();
2774+
2775+
let entries = catalog_for_workspace(root.path(), storage.path()).unwrap();
2776+
let ids = entries
2777+
.iter()
2778+
.map(|entry| entry.id.as_str())
2779+
.collect::<Vec<_>>();
2780+
assert_eq!(ids.len(), 3);
2781+
assert!(ids.contains(&"legacy"));
2782+
assert!(ids.contains(&"top-level"));
2783+
assert!(ids.contains(&"malformed"));
2784+
assert!(!ids.contains(&"subagent"));
2785+
drop((legacy, top_level, malformed, subagent));
2786+
}
2787+
27252788
#[test]
27262789
fn catalog_timestamps_are_rfc3339() {
27272790
assert_eq!(timestamp_rfc3339(0), "1970-01-01T00:00:00.000Z");

0 commit comments

Comments
 (0)