feat(code-interpreter): reuse a warm sandbox per conversation (E2B) (#21664)
## What
The E2B code-interpreter driver created a **fresh sandbox on every
execution** and killed it in `finally`, so every call in a conversation
paid full cold-start and started blank. This PR keeps **one warm sandbox
per conversation** and, on idle, **pauses** it rather than killing it.
## How
- **Discovery without a registry:** the sandbox is tagged with the chat
`threadId` (scoped `workspaceId:threadId`) via E2B **metadata**, found
with `Sandbox.list({ query: { state: ['running','paused'], metadata }
})` and resumed with `Sandbox.connect()` (which auto-resumes a paused
sandbox). E2B is the source of truth — no Redis/DB mapping.
- **Pause/resume (E2B 2.x):** session sandboxes are created with
`lifecycle: { onTimeout: 'pause', autoResume: true }`. When idle they
**pause** — compute billing stops, filesystem **and** kernel/memory
state are preserved — and resume in ~1s on the next call. This replaces
the earlier keepalive approach.
- **No premature pause mid-run:** the sandbox is kept alive for
`max(execution timeout, idle window)`, so a long execution is never
paused underneath itself.
- **Tenant isolation:** discovery filters by the `twentySessionId` tag
and **re-checks it client-side**, so a loose server-side match can never
hand one conversation's warm sandbox (with its files, kernel state,
token) to another.
- **Concurrency:** executions sharing a session are serialized
in-process (one active stream per thread, run as a single job — the chat
resolver queues concurrent messages), so parallel tool calls can't race
the shared kernel.
- **Output isolation:** `/home/user/output` is reset at the start of
each reused run, so a call only returns the artifacts it actually
produced; durable state lives elsewhere and persists.
## SDK upgrade
`@e2b/code-interpreter` **`^1.0.4` → `^2.6.0`** (pulls `e2b@2.x`). The
typed pause/resume API, `lifecycle`, and the `state`/`metadata` list
filter only exist in the 2.x line; 1.x exposed them only as untyped
OpenAPI internals. `Sandbox.list()` is now a paginator (handled).
## Config
| Var | Default | Purpose |
|---|---|---|
| `CODE_INTERPRETER_TIMEOUT_MS` | `300000` | Max single-execution
duration. |
| `CODE_INTERPRETER_IDLE_TIMEOUT_MS` | `300000` | Idle window before the
warm sandbox auto-pauses. |
Reuse is always-on when a session id is present (chat path). The
workflow-agent path and the dev-only `LocalDriver` are unaffected.
## ⚠️ Open item before merge: paused-sandbox GC
E2B retains paused sandboxes **indefinitely** (no TTL). Unlike the old
keepalive path (which auto-killed on idle), pause means a conversation's
sandbox persists after the chat ends — so without garbage collection,
paused sandboxes accumulate (≈ one per historical conversation) and
consume storage. A GC policy is required; the approach + retention
window are being decided (see PR discussion). Also: the E2B runtime path
can't run in CI, so this still needs a **live smoke test** (reuse hit,
idle→pause, resume) and confirmation of paused-storage pricing before
rollout.
## Tests / checks
- Resolver unit tests (`getOrCreateSessionSandbox`): reuse+extend,
create-when-absent, duplicate reaping, connect-failure fallback,
keep-first-connectable-when-earlier-dead, **ignore cross-tenant
metadata**, and **kill-on-timeout-refresh-failure**.
- `nx typecheck twenty-server` (against e2b 2.x), `oxlint --type-aware`,
`oxfmt --check` all clean.
---------
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
+9
-3
@@ -42,7 +42,7 @@ export class CodeInterpreterTool implements Tool {
|
||||
private readonly logger = new Logger(CodeInterpreterTool.name);
|
||||
|
||||
description =
|
||||
'Execute Python code in a sandboxed environment for data analysis, CSV processing, calculations, and chart generation. Returns stdout, stderr, and generated files. Input files are available at /home/user/{filename}. Save output files (charts, reports) to /home/user/output/ using plt.savefig() for matplotlib charts.';
|
||||
'Execute Python code in a sandboxed environment for data analysis, CSV processing, calculations, and chart generation. Returns stdout, stderr, and generated files. Input files are available at /home/user/{filename}. Save output files (charts, reports) to /home/user/output/ using plt.savefig() for matplotlib charts. The sandbox is reused across calls within the same conversation: variables, imports, and any files you write outside /home/user/output persist between calls, so build on earlier results instead of recomputing or re-downloading them. /home/user/output is cleared at the start of every call, so write the files you want returned in the same call that produces them.';
|
||||
|
||||
inputSchema = CodeInterpreterInputZodSchema;
|
||||
|
||||
@@ -82,8 +82,13 @@ export class CodeInterpreterTool implements Tool {
|
||||
parameters: ToolInput,
|
||||
context: ToolExecutionContext,
|
||||
): Promise<ToolOutput> {
|
||||
const { workspaceId, userId, userWorkspaceId, onCodeExecutionUpdate } =
|
||||
context;
|
||||
const {
|
||||
workspaceId,
|
||||
userId,
|
||||
userWorkspaceId,
|
||||
threadId,
|
||||
onCodeExecutionUpdate,
|
||||
} = context;
|
||||
const { code, files } = parameters as CodeInterpreterInput;
|
||||
const executionId = v4();
|
||||
const startTime = Date.now();
|
||||
@@ -128,6 +133,7 @@ export class CodeInterpreterTool implements Tool {
|
||||
TWENTY_SERVER_URL: serverUrl,
|
||||
TWENTY_API_TOKEN: sessionToken,
|
||||
},
|
||||
sessionId: threadId ? `${workspaceId}:${threadId}` : undefined,
|
||||
},
|
||||
{
|
||||
onStdout: (line) => {
|
||||
|
||||
+1
@@ -4,5 +4,6 @@ export type ToolExecutionContext = {
|
||||
workspaceId: string;
|
||||
userId?: string;
|
||||
userWorkspaceId?: string;
|
||||
threadId?: string;
|
||||
onCodeExecutionUpdate?: CodeExecutionStreamEmitter;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user