Skip to content

feat(runtime-api): add PUT /v1/sessions endpoint for engine-based session save#3199

Closed
gaord wants to merge 1 commit into
Hmbown:mainfrom
gaord:feat/runtime-api-session-save
Closed

feat(runtime-api): add PUT /v1/sessions endpoint for engine-based session save#3199
gaord wants to merge 1 commit into
Hmbown:mainfrom
gaord:feat/runtime-api-session-save

Conversation

@gaord

@gaord gaord commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

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

…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
@gaord gaord requested a review from Hmbown as a code owner June 13, 2026 15:57

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1348 to +1349
let total_tokens = self.session.total_usage.input_tokens
+ self.session.total_usage.output_tokens;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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);

Comment on lines +1045 to +1049
threads
.into_iter()
.max_by_key(|t| t.updated_at)
.map(|t| t.id)
.ok_or_else(|| ApiError::bad_request("No threads to save"))?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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 $O(N)$ time. Using threads.first() is cleaner, runs in $O(1)$ time, and avoids consuming the iterator.

Suggested change
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"))?

Comment on lines +1100 to +1102
return Err(ApiError::internal(format!(
"Failed to load session {existing_id}: {e}"
)));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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"));

Comment on lines +1118 to +1120
manager
.save_session(&session)
.map_err(|e| ApiError::internal(format!("Failed to save session: {e}")))?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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"))?;

@Hmbown

Hmbown commented Jun 14, 2026

Copy link
Copy Markdown
Owner

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.

pull Bot pushed a commit to soitun/CodeWhale that referenced this pull request Jun 14, 2026
…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
@Hmbown

Hmbown commented Jun 15, 2026

Copy link
Copy Markdown
Owner

Thank you @gaord — the runtime-api session-save (PUT /v1/sessions) shipped in v0.8.61. Closing as resolved; credited. 🐳

@Hmbown Hmbown closed this Jun 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants