Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "kit"
version = "0.1.115"
version = "0.1.116"
edition = "2024"
rust-version = "1.94.0"
publish = false
Expand Down
2 changes: 1 addition & 1 deletion docs/user/tui-and-sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ kit sessions --root /path/to/project
kit tui --root /path/to/project --resume <session-id>
```

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: <id>` after its answer, and that ID can be continued by either `kit prompt --resume <session-id>` or `kit tui --resume <session-id>`.

Expand Down
12 changes: 11 additions & 1 deletion src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1096,7 +1096,17 @@ impl Runtime {
}

async fn initial_transcript(&self, depth: usize) -> Result<Vec<Item>, 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 {
Expand Down
23 changes: 23 additions & 0 deletions src/runtime/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
63 changes: 63 additions & 0 deletions src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -880,6 +886,22 @@ fn catalog_for_workspace(
Ok(entries)
}

fn catalog_is_subagent(historical_items: &[Vec<Item>], 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<Item>],
current_items: &[Item],
Expand Down Expand Up @@ -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<serde_json::Value>| {
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::<Vec<_>>();
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");
Expand Down