🐛 fix: address Codex review findings for stateful code sessions

- OpenAI-compatible service (packages/api/src/agents/openai/service.ts) now
  derives and passes statefulSessionsAvailable alongside codeEnvAvailable, so
  the feature activates on that route (previously statefulCodeSessions resolved
  false there and createRun never sent toolExecution.sandbox).
- Thread runtime_session_hint through the host file-authoring tools
  (create_file/edit_file/read_file): those host branches return before the
  generic tool path, so readSandboxFile/writeSandboxFile now forward the
  per-conversation hint instead of falling back to the Code API default session.
- StatefulSessions builder toggle clears its form value when Code Interpreter is
  disabled, so a saved agent matches the disabled UI and re-enabling code doesn't
  silently reactivate stateful sessions.
This commit is contained in:
Danny Avila 2026-07-09 10:44:11 -04:00
parent 45c1cd74c6
commit c0919bc726
4 changed files with 53 additions and 2 deletions

View file

@ -990,7 +990,7 @@ const primeFiles = async (options) => {
* @param {ServerRequest} [params.req] - Current authenticated request, used to mint Code API auth.
* @returns {Promise<{content: string} | null>}
*/
async function readSandboxFile({ file_path, session_id, files, req }) {
async function readSandboxFile({ file_path, session_id, files, runtime_session_hint, req }) {
const baseURL = getCodeBaseURL();
if (!baseURL) {
return null;
@ -1006,6 +1006,9 @@ async function readSandboxFile({ file_path, session_id, files, req }) {
if (session_id) {
postData.session_id = session_id;
}
if (runtime_session_hint) {
postData.runtime_session_hint = runtime_session_hint;
}
if (files && files.length > 0) {
postData.files = files;
}
@ -1056,7 +1059,14 @@ async function readSandboxFile({ file_path, session_id, files, req }) {
* @param {ServerRequest} [params.req] - Current authenticated request, used to mint Code API auth.
* @returns {Promise<{stdout?: string, stderr?: string, session_id?: string, files?: Array<Object>} | null>}
*/
async function writeSandboxFile({ file_path, content, session_id, files, req }) {
async function writeSandboxFile({
file_path,
content,
session_id,
files,
runtime_session_hint,
req,
}) {
const baseURL = getCodeBaseURL();
if (!baseURL) {
return null;
@ -1090,6 +1100,9 @@ async function writeSandboxFile({ file_path, content, session_id, files, req })
if (session_id) {
postData.session_id = session_id;
}
if (runtime_session_hint) {
postData.runtime_session_hint = runtime_session_hint;
}
if (files && files.length > 0) {
postData.files = files;
}

View file

@ -1,3 +1,4 @@
import { useEffect } from 'react';
import { useFormContext } from 'react-hook-form';
import { AgentCapabilities } from 'librechat-data-provider';
import {
@ -20,6 +21,15 @@ export default function StatefulSessions() {
const enabled = watch(AgentCapabilities.stateful_code_sessions) ?? false;
const codeEnabled = watch(AgentCapabilities.execute_code);
/** Clear the persisted opt-in when Code Interpreter is turned off so the saved
* agent matches the disabled UI otherwise `composeAgentUpdatePayload` keeps
* `stateful_code_sessions: true` and re-enabling code silently reactivates it. */
useEffect(() => {
if (codeEnabled !== true && enabled) {
setValue(AgentCapabilities.stateful_code_sessions, false, { shouldDirty: true });
}
}, [codeEnabled, enabled, setValue]);
const handleChange = (value: boolean) => {
setValue(AgentCapabilities.stateful_code_sessions, value, { shouldDirty: true });
};

View file

@ -232,6 +232,10 @@ export interface ToolExecuteOptions {
file_path: string;
session_id?: string;
files?: Array<{ id: string; name: string; session_id?: string; storage_session_id?: string }>;
/** Per-conversation stateful runtime-session hint (thread_id); forwarded so a
* host file op that is the first sandbox call joins the same runtime session
* as bash_tool instead of the Code API's default session. */
runtime_session_hint?: string;
req?: ServerRequest;
}) => Promise<{ content: string } | null>;
/**
@ -245,6 +249,8 @@ export interface ToolExecuteOptions {
content: string;
session_id?: string;
files?: Array<{ id: string; name: string; session_id?: string; storage_session_id?: string }>;
/** @see readSandboxFile.runtime_session_hint */
runtime_session_hint?: string;
req?: ServerRequest;
}) => Promise<{
stdout?: string;
@ -1263,6 +1269,7 @@ async function handleSandboxFileFallback(
file_path: filePath,
session_id: ctx?.session_id,
files: ctx?.files,
...(tc.runtimeSessionHint ? { runtime_session_hint: tc.runtimeSessionHint } : {}),
...(req ? { req } : {}),
});
if (!result || result.content == null) {
@ -1431,6 +1438,7 @@ async function loadSandboxTextForAuthoring({
file_path: filePath,
session_id: ctx?.session_id,
files: ctx?.files,
...(tc.runtimeSessionHint ? { runtime_session_hint: tc.runtimeSessionHint } : {}),
...(req ? { req } : {}),
});
if (!result || result.content == null) {
@ -1505,6 +1513,7 @@ async function writeSandboxTextForAuthoring({
content,
session_id: ctx?.session_id,
files: ctx?.files,
...(tc.runtimeSessionHint ? { runtime_session_hint: tc.runtimeSessionHint } : {}),
...(req ? { req } : {}),
});
} catch (error) {

View file

@ -146,6 +146,13 @@ interface InitializeAgentParams {
* skips the expansion (same semantics as the in-repo controllers).
*/
codeEnvAvailable?: boolean;
/**
* Whether the admin-level `stateful_code_sessions` capability is enabled.
* Threaded to `initializeAgent` alongside `codeEnvAvailable` so this
* OpenAI-compatible route resolves stateful sessions identically to the
* in-repo controllers; absent / `undefined` disables the feature.
*/
statefulSessionsAvailable?: boolean;
}
/**
@ -444,6 +451,17 @@ export async function createAgentChatCompletion(
AgentCapabilities.execute_code,
)
: undefined;
/** Mirror `codeEnvAvailable` for the stateful-session gate so an agent with
* `execute_code`, the app `stateful_code_sessions` capability, and its own
* builder opt-in resolves stateful sessions on this route too otherwise
* `statefulCodeSessions` stays false and `createRun` never sends
* `toolExecution.sandbox`. */
const statefulSessionsAvailable =
agentsConfig != null && typeof agentsConfig === 'object'
? ((agentsConfig as { capabilities?: string[] }).capabilities ?? []).includes(
AgentCapabilities.stateful_code_sessions,
)
: undefined;
// Initialize the agent first to check for disableStreaming
const initializedAgent = await deps.initializeAgent({
@ -460,6 +478,7 @@ export async function createAgentChatCompletion(
allowedProviders,
isInitialAgent: true,
codeEnvAvailable,
statefulSessionsAvailable,
});
// Determine if streaming is enabled (check both request and agent config)