Add session continuation symlink fix and /resume command
Fix session detection: - Add save_session_continuation() calls at all session exit points - Sessions now properly create .g3/session symlink for resume detection - Fixes issue where g3 wasn't offering to resume previous sessions Add /resume command: - New list_sessions_for_directory() to scan available sessions - New switch_to_session() method to safely switch between sessions - Shows numbered list with timestamps, context %, and TODO status - Saves current session before switching (can be resumed later) - Restores full context if <80% used, otherwise uses summary - Machine mode supports /resume and /resume <number> Documentation: - Add /clear and /resume to CONTROL_COMMANDS.md - Update /help output with new commands
This commit is contained in:
@@ -858,6 +858,9 @@ async fn run_agent_mode(
|
|||||||
|
|
||||||
let _result = agent.execute_task(final_task, None, true).await?;
|
let _result = agent.execute_task(final_task, None, true).await?;
|
||||||
|
|
||||||
|
// Save session continuation for resume capability
|
||||||
|
agent.save_session_continuation(None);
|
||||||
|
|
||||||
// Don't print completion message for scout agent - it needs the last line
|
// Don't print completion message for scout agent - it needs the last line
|
||||||
// to be the report file path for the research tool to read
|
// to be the report file path for the research tool to read
|
||||||
if agent_name != "scout" {
|
if agent_name != "scout" {
|
||||||
@@ -1261,6 +1264,9 @@ async fn run_autonomous_machine(
|
|||||||
println!("END_AGENT_RESPONSE");
|
println!("END_AGENT_RESPONSE");
|
||||||
println!("TASK_END");
|
println!("TASK_END");
|
||||||
|
|
||||||
|
// Save session continuation for resume capability
|
||||||
|
agent.save_session_continuation(Some(result.response.clone()));
|
||||||
|
|
||||||
println!("AUTONOMOUS_MODE_ENDED");
|
println!("AUTONOMOUS_MODE_ENDED");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1299,6 +1305,8 @@ async fn run_with_console_mode(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
output.print_smart(&result.response);
|
output.print_smart(&result.response);
|
||||||
|
// Save session continuation for resume capability
|
||||||
|
agent.save_session_continuation(Some(result.response.clone()));
|
||||||
} else {
|
} else {
|
||||||
// Interactive mode (default)
|
// Interactive mode (default)
|
||||||
run_interactive(
|
run_interactive(
|
||||||
@@ -1347,6 +1355,8 @@ async fn run_with_machine_mode(
|
|||||||
println!("AGENT_RESPONSE:");
|
println!("AGENT_RESPONSE:");
|
||||||
println!("{}", result.response);
|
println!("{}", result.response);
|
||||||
println!("END_AGENT_RESPONSE");
|
println!("END_AGENT_RESPONSE");
|
||||||
|
// Save session continuation for resume capability
|
||||||
|
agent.save_session_continuation(Some(result.response.clone()));
|
||||||
} else {
|
} else {
|
||||||
// Interactive mode
|
// Interactive mode
|
||||||
run_interactive_machine(agent, cli.show_prompt, cli.show_code).await?;
|
run_interactive_machine(agent, cli.show_prompt, cli.show_code).await?;
|
||||||
@@ -1700,6 +1710,7 @@ async fn run_interactive<W: UiWriter>(
|
|||||||
output.print(" /thinnify - Trigger context thinning (replaces large tool results with file references)");
|
output.print(" /thinnify - Trigger context thinning (replaces large tool results with file references)");
|
||||||
output.print(" /skinnify - Trigger full context thinning (like /thinnify but for entire context, not just first third)");
|
output.print(" /skinnify - Trigger full context thinning (like /thinnify but for entire context, not just first third)");
|
||||||
output.print(" /clear - Clear session and start fresh (discards continuation artifacts)");
|
output.print(" /clear - Clear session and start fresh (discards continuation artifacts)");
|
||||||
|
output.print(" /resume - List and switch to a previous session");
|
||||||
output.print(
|
output.print(
|
||||||
" /readme - Reload README.md and AGENTS.md from disk",
|
" /readme - Reload README.md and AGENTS.md from disk",
|
||||||
);
|
);
|
||||||
@@ -1762,6 +1773,72 @@ async fn run_interactive<W: UiWriter>(
|
|||||||
output.print(&stats);
|
output.print(&stats);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
"/resume" => {
|
||||||
|
output.print("📋 Scanning for available sessions...");
|
||||||
|
|
||||||
|
match g3_core::list_sessions_for_directory() {
|
||||||
|
Ok(sessions) => {
|
||||||
|
if sessions.is_empty() {
|
||||||
|
output.print("No sessions found for this directory.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current session ID to mark it
|
||||||
|
let current_session_id = agent.get_session_id().map(|s| s.to_string());
|
||||||
|
|
||||||
|
output.print("");
|
||||||
|
output.print("Available sessions:");
|
||||||
|
for (i, session) in sessions.iter().enumerate() {
|
||||||
|
let time_str = g3_core::format_session_time(&session.created_at);
|
||||||
|
let context_str = format!("{:.0}%", session.context_percentage);
|
||||||
|
let current_marker = if current_session_id.as_deref() == Some(&session.session_id) {
|
||||||
|
" (current)"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
let todo_marker = if session.has_incomplete_todos() { " 📝" } else { "" };
|
||||||
|
|
||||||
|
// Truncate session ID for display
|
||||||
|
let display_id = if session.session_id.len() > 40 {
|
||||||
|
format!("{}...", &session.session_id[..40])
|
||||||
|
} else {
|
||||||
|
session.session_id.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
output.print(&format!(
|
||||||
|
" {}. [{}] {} ({}){}{}",
|
||||||
|
i + 1, time_str, display_id, context_str, todo_marker, current_marker
|
||||||
|
));
|
||||||
|
}
|
||||||
|
output.print("");
|
||||||
|
output.print("Enter session number to resume (or press Enter to cancel):");
|
||||||
|
|
||||||
|
// Read user selection
|
||||||
|
if let Ok(selection) = rl.readline("> ") {
|
||||||
|
let selection = selection.trim();
|
||||||
|
if selection.is_empty() {
|
||||||
|
output.print("Resume cancelled.");
|
||||||
|
} else if let Ok(num) = selection.parse::<usize>() {
|
||||||
|
if num >= 1 && num <= sessions.len() {
|
||||||
|
let selected = &sessions[num - 1];
|
||||||
|
output.print(&format!("🔄 Switching to session: {}", selected.session_id));
|
||||||
|
match agent.switch_to_session(selected) {
|
||||||
|
Ok(true) => output.print("✅ Full context restored from session."),
|
||||||
|
Ok(false) => output.print("✅ Session restored from summary."),
|
||||||
|
Err(e) => output.print(&format!("❌ Error restoring session: {}", e)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output.print("Invalid selection.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output.print("Invalid input. Please enter a number.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => output.print(&format!("❌ Error listing sessions: {}", e)),
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
output.print(&format!(
|
output.print(&format!(
|
||||||
"❌ Unknown command: {}. Type /help for available commands.",
|
"❌ Unknown command: {}. Type /help for available commands.",
|
||||||
@@ -1804,6 +1881,9 @@ async fn run_interactive<W: UiWriter>(
|
|||||||
let _ = rl.save_history(history_path);
|
let _ = rl.save_history(history_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save session continuation for resume capability
|
||||||
|
agent.save_session_continuation(None);
|
||||||
|
|
||||||
output.print("👋 Goodbye!");
|
output.print("👋 Goodbye!");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1986,10 +2066,62 @@ async fn run_interactive_machine(
|
|||||||
}
|
}
|
||||||
"/help" => {
|
"/help" => {
|
||||||
println!("COMMAND: help");
|
println!("COMMAND: help");
|
||||||
println!("AVAILABLE_COMMANDS: /compact /thinnify /skinnify /clear /readme /stats /help");
|
println!("AVAILABLE_COMMANDS: /compact /thinnify /skinnify /clear /resume /readme /stats /help");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
"/resume" => {
|
||||||
|
println!("COMMAND: resume");
|
||||||
|
match g3_core::list_sessions_for_directory() {
|
||||||
|
Ok(sessions) => {
|
||||||
|
if sessions.is_empty() {
|
||||||
|
println!("RESULT: No sessions found");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("SESSIONS_START");
|
||||||
|
for (i, session) in sessions.iter().enumerate() {
|
||||||
|
let time_str = g3_core::format_session_time(&session.created_at);
|
||||||
|
let has_todos = if session.has_incomplete_todos() { "true" } else { "false" };
|
||||||
|
println!(
|
||||||
|
"SESSION: {} | {} | {} | {:.0}% | {}",
|
||||||
|
i + 1,
|
||||||
|
session.session_id,
|
||||||
|
time_str,
|
||||||
|
session.context_percentage,
|
||||||
|
has_todos
|
||||||
|
);
|
||||||
|
}
|
||||||
|
println!("SESSIONS_END");
|
||||||
|
println!("HINT: Use /resume <number> to switch to a session");
|
||||||
|
}
|
||||||
|
Err(e) => println!("ERROR: {}", e),
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
// Check for /resume <number> pattern
|
||||||
|
if input.starts_with("/resume ") {
|
||||||
|
let num_str = input.strip_prefix("/resume ").unwrap().trim();
|
||||||
|
if let Ok(num) = num_str.parse::<usize>() {
|
||||||
|
println!("COMMAND: resume {}", num);
|
||||||
|
match g3_core::list_sessions_for_directory() {
|
||||||
|
Ok(sessions) => {
|
||||||
|
if num >= 1 && num <= sessions.len() {
|
||||||
|
let selected = &sessions[num - 1];
|
||||||
|
match agent.switch_to_session(selected) {
|
||||||
|
Ok(true) => println!("RESULT: Full context restored from session {}", selected.session_id),
|
||||||
|
Ok(false) => println!("RESULT: Session {} restored from summary", selected.session_id),
|
||||||
|
Err(e) => println!("ERROR: {}", e),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("ERROR: Invalid session number");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => println!("ERROR: {}", e),
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
println!("ERROR: Unknown command: {}", input);
|
println!("ERROR: Unknown command: {}", input);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -2015,6 +2147,9 @@ async fn run_interactive_machine(
|
|||||||
let _ = rl.save_history(history_path);
|
let _ = rl.save_history(history_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save session continuation for resume capability
|
||||||
|
agent.save_session_continuation(None);
|
||||||
|
|
||||||
println!("INTERACTIVE_MODE_ENDED");
|
println!("INTERACTIVE_MODE_ENDED");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -2938,5 +3073,8 @@ Remember: Be clear in your review and concise in your feedback. APPROVE iff the
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save session continuation for resume capability
|
||||||
|
agent.save_session_continuation(None);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ pub mod webdriver_session;
|
|||||||
pub use task_result::TaskResult;
|
pub use task_result::TaskResult;
|
||||||
pub use retry::{RetryConfig, RetryResult, execute_with_retry, retry_operation};
|
pub use retry::{RetryConfig, RetryResult, execute_with_retry, retry_operation};
|
||||||
pub use feedback_extraction::{ExtractedFeedback, FeedbackSource, FeedbackExtractionConfig, extract_coach_feedback};
|
pub use feedback_extraction::{ExtractedFeedback, FeedbackSource, FeedbackExtractionConfig, extract_coach_feedback};
|
||||||
pub use session_continuation::{SessionContinuation, load_continuation, save_continuation, clear_continuation, has_valid_continuation, get_session_dir, load_context_from_session_log, find_incomplete_agent_session};
|
pub use session_continuation::{SessionContinuation, load_continuation, save_continuation, clear_continuation, has_valid_continuation, get_session_dir, load_context_from_session_log, find_incomplete_agent_session, list_sessions_for_directory, format_session_time};
|
||||||
|
|
||||||
// Re-export context window types
|
// Re-export context window types
|
||||||
pub use context_window::{ContextWindow, ThinScope};
|
pub use context_window::{ContextWindow, ThinScope};
|
||||||
@@ -1528,6 +1528,42 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Switch to a different session, saving the current one first.
|
||||||
|
/// This discards the current in-memory state and loads the new session.
|
||||||
|
pub fn switch_to_session(
|
||||||
|
&mut self,
|
||||||
|
continuation: &crate::session_continuation::SessionContinuation,
|
||||||
|
) -> Result<bool> {
|
||||||
|
// Save current session first (so it can be resumed later)
|
||||||
|
self.save_session_continuation(None);
|
||||||
|
|
||||||
|
// Reset session-specific metrics
|
||||||
|
self.thinning_events.clear();
|
||||||
|
self.compaction_events.clear();
|
||||||
|
self.first_token_times.clear();
|
||||||
|
self.tool_call_metrics.clear();
|
||||||
|
self.tool_call_count = 0;
|
||||||
|
self.pending_90_compaction = false;
|
||||||
|
|
||||||
|
// Update session ID to the new session
|
||||||
|
self.session_id = Some(continuation.session_id.clone());
|
||||||
|
|
||||||
|
// Update agent mode info from continuation
|
||||||
|
self.is_agent_mode = continuation.is_agent_mode;
|
||||||
|
self.agent_name = continuation.agent_name.clone();
|
||||||
|
|
||||||
|
// Load TODO content from the new session if available
|
||||||
|
if let Some(ref todo) = continuation.todo_snapshot {
|
||||||
|
// Use blocking write since we're in a sync context
|
||||||
|
if let Ok(mut guard) = self.todo_content.try_write() {
|
||||||
|
*guard = todo.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore context from the continuation
|
||||||
|
self.restore_from_continuation(continuation)
|
||||||
|
}
|
||||||
|
|
||||||
async fn stream_completion(
|
async fn stream_completion(
|
||||||
&mut self,
|
&mut self,
|
||||||
request: CompletionRequest,
|
request: CompletionRequest,
|
||||||
|
|||||||
@@ -375,6 +375,71 @@ pub fn find_incomplete_agent_session(agent_name: &str) -> Result<Option<SessionC
|
|||||||
Ok(candidates.into_iter().next())
|
Ok(candidates.into_iter().next())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// List all available sessions in the current working directory.
|
||||||
|
/// Returns sessions sorted by creation time (most recent first).
|
||||||
|
pub fn list_sessions_for_directory() -> Result<Vec<SessionContinuation>> {
|
||||||
|
let sessions_dir = get_sessions_dir();
|
||||||
|
|
||||||
|
if !sessions_dir.exists() {
|
||||||
|
debug!("Sessions directory does not exist: {:?}", sessions_dir);
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_dir = std::env::current_dir()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let mut sessions: Vec<SessionContinuation> = Vec::new();
|
||||||
|
|
||||||
|
// Scan all session directories
|
||||||
|
for entry in std::fs::read_dir(&sessions_dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if !path.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for latest.json in this session directory
|
||||||
|
let latest_path = path.join(CONTINUATION_FILENAME);
|
||||||
|
if !latest_path.exists() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to load the continuation
|
||||||
|
let json = match std::fs::read_to_string(&latest_path) {
|
||||||
|
Ok(j) => j,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let continuation: SessionContinuation = match serde_json::from_str(&json) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => continue, // Skip sessions with old format
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only include sessions from the current working directory
|
||||||
|
if continuation.working_directory == current_dir {
|
||||||
|
sessions.push(continuation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by created_at descending (most recent first)
|
||||||
|
sessions.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||||
|
|
||||||
|
Ok(sessions)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a session's created_at timestamp for display
|
||||||
|
pub fn format_session_time(created_at: &str) -> String {
|
||||||
|
match chrono::DateTime::parse_from_rfc3339(created_at) {
|
||||||
|
Ok(dt) => {
|
||||||
|
let local: chrono::DateTime<chrono::Local> = dt.into();
|
||||||
|
local.format("%Y-%m-%d %H:%M").to_string()
|
||||||
|
}
|
||||||
|
Err(_) => created_at.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ Control commands are special commands you can use during an interactive G3 sessi
|
|||||||
| `/compact` | Manually trigger conversation compaction |
|
| `/compact` | Manually trigger conversation compaction |
|
||||||
| `/thinnify` | Replace large tool results with file references (first third) |
|
| `/thinnify` | Replace large tool results with file references (first third) |
|
||||||
| `/skinnify` | Full context thinning (entire context window) |
|
| `/skinnify` | Full context thinning (entire context window) |
|
||||||
|
| `/clear` | Clear session and start fresh |
|
||||||
|
| `/resume` | List and switch to a previous session |
|
||||||
| `/readme` | Reload README.md and AGENTS.md from disk |
|
| `/readme` | Reload README.md and AGENTS.md from disk |
|
||||||
| `/stats` | Show detailed context and performance statistics |
|
| `/stats` | Show detailed context and performance statistics |
|
||||||
| `/help` | Display all available control commands |
|
| `/help` | Display all available control commands |
|
||||||
@@ -105,6 +107,74 @@ g3> /skinnify
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## /clear
|
||||||
|
|
||||||
|
Clear the current session and start fresh.
|
||||||
|
|
||||||
|
**When to use**:
|
||||||
|
- You want to start a completely new task
|
||||||
|
- The current context is cluttered or confused
|
||||||
|
- You want to discard all conversation history
|
||||||
|
|
||||||
|
**What it does**:
|
||||||
|
1. Clears all conversation history (keeps system prompt)
|
||||||
|
2. Removes the session continuation symlink
|
||||||
|
3. Resets context to initial state
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```
|
||||||
|
g3> /clear
|
||||||
|
🧹 Clearing session...
|
||||||
|
✅ Session cleared. Starting fresh.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes**:
|
||||||
|
- This is irreversible for the current session
|
||||||
|
- Previous session data remains in `.g3/sessions/` and can be resumed with `/resume`
|
||||||
|
- Use when you want a clean slate
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## /resume
|
||||||
|
|
||||||
|
List available sessions and switch to a previous one.
|
||||||
|
|
||||||
|
**When to use**:
|
||||||
|
- You want to continue work from a previous session
|
||||||
|
- You accidentally cleared or lost context
|
||||||
|
- You want to switch between different tasks/sessions
|
||||||
|
|
||||||
|
**What it does**:
|
||||||
|
1. Scans `.g3/sessions/` for sessions in the current directory
|
||||||
|
2. Displays a numbered list with timestamps and context usage
|
||||||
|
3. Prompts for selection
|
||||||
|
4. Saves current session before switching
|
||||||
|
5. Restores the selected session's context
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```
|
||||||
|
g3> /resume
|
||||||
|
📋 Scanning for available sessions...
|
||||||
|
|
||||||
|
Available sessions:
|
||||||
|
1. [2025-01-11 14:30] implement_auth_feature_abc123 (45%) 📝
|
||||||
|
2. [2025-01-11 10:15] fix_bug_in_parser_def456 (23%)
|
||||||
|
3. [2025-01-10 16:45] refactor_database_layer_ghi789 (67%)
|
||||||
|
|
||||||
|
Enter session number to resume (or press Enter to cancel):
|
||||||
|
> 1
|
||||||
|
🔄 Switching to session: implement_auth_feature_abc123
|
||||||
|
✅ Full context restored from session.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes**:
|
||||||
|
- Sessions marked with 📝 have incomplete TODO items
|
||||||
|
- Current session is marked with "(current)"
|
||||||
|
- Only sessions from the current working directory are shown
|
||||||
|
- Full context is restored if usage was <80%, otherwise summary is used
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## /readme
|
## /readme
|
||||||
|
|
||||||
Reload README.md and AGENTS.md from disk without restarting.
|
Reload README.md and AGENTS.md from disk without restarting.
|
||||||
@@ -174,6 +244,8 @@ g3> /help
|
|||||||
/compact - Summarize conversation to reduce context
|
/compact - Summarize conversation to reduce context
|
||||||
/thinnify - Replace large tool results with file refs
|
/thinnify - Replace large tool results with file refs
|
||||||
/skinnify - Full context thinning (entire window)
|
/skinnify - Full context thinning (entire window)
|
||||||
|
/clear - Clear session and start fresh
|
||||||
|
/resume - List and switch to a previous session
|
||||||
/readme - Reload README.md and AGENTS.md
|
/readme - Reload README.md and AGENTS.md
|
||||||
/stats - Show context and performance statistics
|
/stats - Show context and performance statistics
|
||||||
/help - Show this help message
|
/help - Show this help message
|
||||||
|
|||||||
Reference in New Issue
Block a user