feat(runtime-api): add PUT /v1/sessions endpoint for engine-based session save#3199
feat(runtime-api): add PUT /v1/sessions endpoint for engine-based session save#3199gaord wants to merge 1 commit into
Conversation
…sion save Add a new PUT /v1/sessions endpoint that saves a thread's current engine state as a session, complementing the existing POST /v1/sessions which reconstructs messages from stored turn items. The new endpoint asks the engine for its live session snapshot via a oneshot channel, so token counts and message ordering are authoritative rather than reconstructed. This matches TUI's build_session_snapshot behavior. Changes: - ops.rs: add SessionSnapshot struct and Op::GetSessionSnapshot variant - engine.rs: handle GetSessionSnapshot in the engine loop - engine/handle.rs: add get_session_snapshot() method - runtime_threads.rs: expose get_engine() as public wrapper - runtime_api.rs: add PUT /v1/sessions route and save_current_session handler Also fixes the Greptile review issue where load_session errors were silently swallowed: only io::ErrorKind::NotFound falls back to creating a new session; other I/O errors (e.g. PermissionDenied) are now propagated. Ref: Hmbown#2808
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
There was a problem hiding this comment.
Code Review
This pull request introduces a new PUT /v1/sessions endpoint to save a thread's current engine state as a session by requesting a live snapshot directly from the engine. Key changes include adding the GetSessionSnapshot operation to the engine, exposing get_engine in RuntimeThreadManager, and implementing the save_current_session handler. Feedback focuses on casting token counts to u64 to prevent overflow, optimizing the retrieval of the most recent thread by using threads.first() instead of max_by_key, and using the map_session_err helper to ensure consistent and accurate API error responses when loading or saving sessions.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| let total_tokens = self.session.total_usage.input_tokens | ||
| + self.session.total_usage.output_tokens; |
There was a problem hiding this comment.
To prevent potential overflow and ensure type compatibility with SessionSnapshot::total_tokens (which is u64), cast the input_tokens and output_tokens to u64 before performing the addition.
| let total_tokens = self.session.total_usage.input_tokens | |
| + self.session.total_usage.output_tokens; | |
| let total_tokens = u64::from(self.session.total_usage.input_tokens) | |
| + u64::from(self.session.total_usage.output_tokens); |
| threads | ||
| .into_iter() | ||
| .max_by_key(|t| t.updated_at) | ||
| .map(|t| t.id) | ||
| .ok_or_else(|| ApiError::bad_request("No threads to save"))? |
There was a problem hiding this comment.
Since list_threads already returns threads sorted descending by updated_at (most recent first), finding the maximum via max_by_key is redundant and runs in threads.first() is cleaner, runs in
| threads | |
| .into_iter() | |
| .max_by_key(|t| t.updated_at) | |
| .map(|t| t.id) | |
| .ok_or_else(|| ApiError::bad_request("No threads to save"))? | |
| threads | |
| .first() | |
| .map(|t| t.id.clone()) | |
| .ok_or_else(|| ApiError::bad_request("No threads to save"))? |
| return Err(ApiError::internal(format!( | ||
| "Failed to load session {existing_id}: {e}" | ||
| ))); |
There was a problem hiding this comment.
Use the existing map_session_err helper function to map the error. This ensures that client-side errors (like InvalidInput for malformed session IDs) are correctly translated to 400 Bad Request instead of generic 500 Internal Server Error responses.
return Err(map_session_err(existing_id, e, "load"));| manager | ||
| .save_session(&session) | ||
| .map_err(|e| ApiError::internal(format!("Failed to save session: {e}")))?; |
There was a problem hiding this comment.
Use the existing map_session_err helper function to map the error when saving the session. This ensures that any validation or I/O errors are mapped consistently and client-side errors are correctly surfaced as 400 Bad Request instead of 500 Internal Server Error.
| manager | |
| .save_session(&session) | |
| .map_err(|e| ApiError::internal(format!("Failed to save session: {e}")))?; | |
| manager | |
| .save_session(&session) | |
| .map_err(|e| map_session_err(&session.metadata.id, e, "save"))?; |
|
Thank you @gaord! The PUT /v1/sessions snapshot path — using a oneshot so it does not contend with the SSE stream — is a nice design. Landed in the v0.8.61 release prep with your authorship after a tiny rustfmt pass. Appreciated. |
…sion save Add a new PUT /v1/sessions endpoint that saves a thread's current engine state as a session, complementing the existing POST /v1/sessions which reconstructs messages from stored turn items. The new endpoint asks the engine for its live session snapshot via a oneshot channel, so token counts and message ordering are authoritative rather than reconstructed. This matches TUI's build_session_snapshot behavior. Changes: - ops.rs: add SessionSnapshot struct and Op::GetSessionSnapshot variant - engine.rs: handle GetSessionSnapshot in the engine loop - engine/handle.rs: add get_session_snapshot() method - runtime_threads.rs: expose get_engine() as public wrapper - runtime_api.rs: add PUT /v1/sessions route and save_current_session handler Also fixes the Greptile review issue where load_session errors were silently swallowed: only io::ErrorKind::NotFound falls back to creating a new session; other I/O errors (e.g. PermissionDenied) are now propagated. Ref: Hmbown#2808 Harvested-from: PR Hmbown#3199 by @gaord
|
Thank you @gaord — the runtime-api session-save (PUT /v1/sessions) shipped in v0.8.61. Closing as resolved; credited. 🐳 |
Focused slice from #2808: add a PUT /v1/sessions endpoint that saves a thread's current engine state as a session. This is the session-save slice that @Hmbown identified as the next harvest target.
New endpoint: PUT /v1/sessions saves a thread live engine state as a session via a oneshot channel, so token counts and message ordering are authoritative. Unlike POST /v1/sessions (which reconstructs from turn items), this matches TUI build_session_snapshot behavior. Backward compatible: existing POST endpoint is preserved.
Implementation: SessionSnapshot struct + Op::GetSessionSnapshot variant (ops.rs), engine loop handler (engine.rs), get_session_snapshot method (handle.rs), get_engine public wrapper (runtime_threads.rs), PUT /v1/sessions route + save_current_session handler (runtime_api.rs).
Bug fix: load_session errors were silently swallowed by Err(_) wildcard. Only io::ErrorKind::NotFound now falls back to creating a new session; other I/O errors (e.g. PermissionDenied) are propagated.
Not included (need atomicity tests): undo, patch-undo, retry, restore-snapshot, ToolCall/FileChange reconstruction.
Ref: #2808