feat(copilot): E2B sandbox auto-pause between turns to eliminate idle billing - #12330
Conversation
- Upgrade e2b-code-interpreter 1.x → 2.x (pulls in e2b v2 with pause/resume)
- Create sandboxes with lifecycle={on_timeout: pause} as safety net
- Add pause_sandbox() helper to pause between turns via AsyncSandbox.connect/pause
- Call pause_sandbox() in sdk/service.py finally block after each CoPilot turn
- Fix code_executor.py type: ignore for e2b v2 namespace shadowing issue
- Add TestPauseSandbox tests; assert lifecycle param on creation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThis PR modifies E2B sandbox lifecycle management by introducing per-turn pausing for billing control. It updates Changes
Sequence Diagram(s)sequenceDiagram
actor Client
participant Service
participant Config
participant E2B as E2B Sandbox
participant Transcript as Transcript Upload
Client->>Service: Start turn
Service->>Config: Check e2b_active
Config-->>Service: true (enabled + API key present)
Service->>E2B: get_or_create_sandbox(sandbox_timeout, redis_ttl)
E2B-->>Service: AsyncSandbox (with lifecycle pause)
Service->>Service: _next_msg (compute response)
Service->>E2B: pause_sandbox()
E2B-->>Service: paused (or warning logged)
Service->>Transcript: Upload turn transcript
Transcript-->>Service: complete
Service->>E2B: pause_sandbox() again
E2B-->>Service: paused (or warning logged)
Service-->>Client: Turn complete
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🔍 PR Overlap DetectionThis check compares your PR against all other open PRs targeting the same branch to detect potential merge conflicts early. 🔴 Merge Conflicts DetectedThe following PRs have been tested and will have merge conflicts if merged after this PR. Consider coordinating with the authors.
🟢 Low Risk — File Overlap OnlyThese PRs touch the same files but different sections (click to expand)
Summary: 5 conflict(s), 0 medium risk, 11 low risk (out of 16 PRs with file overlap) Auto-generated on push. Ignores: |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
autogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py (1)
126-136:⚠️ Potential issue | 🟠 MajorKeep the Redis sandbox mapping alive longer than the E2B timeout.
Line 136 still uses
setex(..., timeout, ...), but the sandbox now pauses attimeoutinstead of terminating. That means the Redis key expires exactly when the paused sandbox still exists, so the next turn creates a fresh sandbox and loses prior filesystem state, andkill_sandbox()can no longer clean up the old paused one.💡 Proposed fix
- await redis.setex(redis_key, timeout, sandbox.sandbox_id) + # The sandbox now survives the runtime timeout by pausing, so keep the + # session→sandbox mapping until explicit cleanup (or use a longer + # session-retention TTL instead of the sandbox runtime timeout). + await redis.set(redis_key, sandbox.sandbox_id)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@autogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py` around lines 126 - 136, The Redis mapping is being set with redis.setex(redis_key, timeout, sandbox.sandbox_id) but the sandbox now pauses at timeout instead of being destroyed, so the key must outlive the E2B timeout; update the code that sets the Redis TTL (the redis.setex call in e2b_sandbox.py after AsyncSandbox.create) to use a longer TTL than timeout (e.g., timeout plus a configurable grace period or multiply by a factor), and ensure the value stored is still sandbox.sandbox_id so kill_sandbox() can find and clean paused sandboxes; make this TTL configurable via a constant or parameter rather than hardcoding.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@autogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py`:
- Around line 164-180: The calls to AsyncSandbox.connect and sandbox.pause can
hang indefinitely; wrap both awaits with a timeout (e.g., using asyncio.wait_for
or asyncio.timeout) so the connect and pause ops don't block the service; update
the AsyncSandbox.connect(...) and sandbox.pause() invocations in the pause flow
to use bounded timeouts and handle TimeoutError in the except block to log a
clear timeout message and return False.
---
Outside diff comments:
In `@autogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py`:
- Around line 126-136: The Redis mapping is being set with
redis.setex(redis_key, timeout, sandbox.sandbox_id) but the sandbox now pauses
at timeout instead of being destroyed, so the key must outlive the E2B timeout;
update the code that sets the Redis TTL (the redis.setex call in e2b_sandbox.py
after AsyncSandbox.create) to use a longer TTL than timeout (e.g., timeout plus
a configurable grace period or multiply by a factor), and ensure the value
stored is still sandbox.sandbox_id so kill_sandbox() can find and clean paused
sandboxes; make this TTL configurable via a constant or parameter rather than
hardcoding.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 433ead1e-3e3a-4e96-b595-912479827092
⛔ Files ignored due to path filters (1)
autogpt_platform/backend/poetry.lockis excluded by!**/*.lock
📒 Files selected for processing (5)
autogpt_platform/backend/backend/blocks/code_executor.pyautogpt_platform/backend/backend/copilot/sdk/service.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.pyautogpt_platform/backend/pyproject.toml
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: types
- GitHub Check: Seer Code Review
- GitHub Check: Check PR Status
🧰 Additional context used
📓 Path-based instructions (8)
autogpt_platform/backend/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
autogpt_platform/backend/**/*.py: Use Python 3.11 (required; managed by Poetry via pyproject.toml) for backend development
Always run 'poetry run format' (Black + isort) before linting in backend development
Always run 'poetry run lint' (ruff) after formatting in backend development
Files:
autogpt_platform/backend/backend/blocks/code_executor.pyautogpt_platform/backend/backend/copilot/sdk/service.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py
autogpt_platform/backend/backend/blocks/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
autogpt_platform/backend/backend/blocks/**/*.py: Inherit from 'Block' base class with input/output schemas when adding new blocks in backend
Implement 'run' method with proper error handling in backend blocks
Generate block UUID using 'uuid.uuid4()' when creating new blocks in backend
Write tests alongside block implementation when adding new blocks in backend
Files:
autogpt_platform/backend/backend/blocks/code_executor.py
autogpt_platform/backend/**/*.{py,txt}
📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)
Use
poetry runprefix for all Python commands, including testing, linting, formatting, and migrations
Files:
autogpt_platform/backend/backend/blocks/code_executor.pyautogpt_platform/backend/backend/copilot/sdk/service.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py
autogpt_platform/backend/backend/**/*.py
📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)
Use Prisma ORM for database operations in PostgreSQL with pgvector for embeddings
Files:
autogpt_platform/backend/backend/blocks/code_executor.pyautogpt_platform/backend/backend/copilot/sdk/service.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py
autogpt_platform/backend/backend/blocks/*.py
📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)
autogpt_platform/backend/backend/blocks/*.py: When creating new blocks, inherit from theBlockbase class and define input/output schemas usingBlockSchema
Implement blocks with an asyncrunmethod and generate unique block IDs usinguuid.uuid4()
When working with files in blocks, usestore_media_file()frombackend.util.filewith appropriatereturn_formatparameter:for_local_processingfor local tools,for_external_apifor external APIs,for_block_outputfor block outputs
Always usefor_block_outputformat instore_media_file()for block outputs unless there is a specific reason not to
Never hardcode workspace checks when usingstore_media_file()- letfor_block_outputhandle context adaptation automatically
When adding new blocks, analyze block interfaces to ensure inputs and outputs tie well together for productive graph-based editor connections
Files:
autogpt_platform/backend/backend/blocks/code_executor.py
autogpt_platform/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
Format Python code with
poetry run format
Files:
autogpt_platform/backend/backend/blocks/code_executor.pyautogpt_platform/backend/backend/copilot/sdk/service.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py
autogpt_platform/backend/**/*_test.py
📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)
autogpt_platform/backend/**/*_test.py: Always review snapshot changes withgit diffbefore committing when updating snapshots withpoetry run pytest --snapshot-update
Use pytest with snapshot testing for API responses in test files
Colocate test files with source files using the*_test.pynaming convention
Files:
autogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.py
autogpt_platform/backend/**/*test*.py
📄 CodeRabbit inference engine (AGENTS.md)
Run
poetry run testfor backend testing (runs pytest with docker based postgres + prisma)
Files:
autogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.py
🧠 Learnings (8)
📚 Learning: 2026-01-23T19:58:10.520Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 11796
File: autogpt_platform/backend/backend/blocks/video/loop.py:80-87
Timestamp: 2026-01-23T19:58:10.520Z
Learning: Ensure MoviePy is constrained to version ^2.1.2 (2.x) in pyproject.toml files where MoviePy is declared, so the backend video processing relies on a compatible API. This should cover all relevant pyproject.toml files (e.g., autogpt_platform/backend/pyproject.toml) to maintain consistency.
Applied to files:
autogpt_platform/backend/pyproject.toml
📚 Learning: 2026-02-04T16:49:42.490Z
Learnt from: CR
Repo: Significant-Gravitas/AutoGPT PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-04T16:49:42.490Z
Learning: Applies to autogpt_platform/backend/**/*.py : Use Python 3.11 (required; managed by Poetry via pyproject.toml) for backend development
Applied to files:
autogpt_platform/backend/pyproject.toml
📚 Learning: 2026-03-04T23:58:09.319Z
Learnt from: Pwuts
Repo: Significant-Gravitas/AutoGPT PR: 12284
File: autogpt_platform/frontend/src/app/api/openapi.json:11897-11900
Timestamp: 2026-03-04T23:58:09.319Z
Learning: Repo: Significant-Gravitas/AutoGPT — PR `#12284`
Backend/frontend OpenAPI codegen convention: In backend/api/features/store/model.py, the StoreSubmission and StoreSubmissionAdminView models define submitted_at: datetime | None, changes_summary: str | None, and instructions: str | None with no default. This is intentional to produce “required but nullable” fields in OpenAPI (properties appear in required[] and use anyOf [type, null]). This matches Prisma’s submittedAt DateTime? and changesSummary String?. Do not flag this as a required/nullable mismatch.
Applied to files:
autogpt_platform/backend/pyproject.toml
📚 Learning: 2026-02-26T17:02:22.448Z
Learnt from: Pwuts
Repo: Significant-Gravitas/AutoGPT PR: 12211
File: .pre-commit-config.yaml:160-179
Timestamp: 2026-02-26T17:02:22.448Z
Learning: Keep the pre-commit hook pattern broad for autogpt_platform/backend to ensure OpenAPI schema changes are captured. Do not narrow to backend/api/ alone, since the generated schema depends on Pydantic models across multiple directories (backend/data/, backend/blocks/, backend/copilot/, backend/integrations/, backend/util/). Narrowing could miss schema changes and cause frontend type desynchronization.
Applied to files:
autogpt_platform/backend/pyproject.tomlautogpt_platform/backend/backend/blocks/code_executor.pyautogpt_platform/backend/backend/copilot/sdk/service.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py
📚 Learning: 2026-02-05T04:11:00.596Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 11796
File: autogpt_platform/backend/backend/blocks/video/concat.py:3-4
Timestamp: 2026-02-05T04:11:00.596Z
Learning: In autogpt_platform/backend/backend/blocks/**/*.py, when creating a new block, generate a UUID once with uuid.uuid4() and hard-code the resulting string as the block's id parameter. Do not call uuid.uuid4() at runtime; IDs must be constant across all imports and runs to ensure stability.
Applied to files:
autogpt_platform/backend/backend/blocks/code_executor.py
📚 Learning: 2026-03-05T15:42:08.207Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 12297
File: .claude/skills/backend-check/SKILL.md:14-16
Timestamp: 2026-03-05T15:42:08.207Z
Learning: In Python files under autogpt_platform/backend (recursively), rely on poetry run format to perform formatting (Black + isort) and linting (ruff). Do not run poetry run lint as a separate step after poetry run format, since format already includes linting checks.
Applied to files:
autogpt_platform/backend/backend/blocks/code_executor.pyautogpt_platform/backend/backend/copilot/sdk/service.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py
📚 Learning: 2026-03-04T08:04:35.881Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12273
File: autogpt_platform/backend/backend/copilot/tools/workspace_files.py:216-220
Timestamp: 2026-03-04T08:04:35.881Z
Learning: In the AutoGPT Copilot backend, ensure that SVG images are not treated as vision image types by excluding 'image/svg+xml' from INLINEABLE_MIME_TYPES and MULTIMODAL_TYPES in tool_adapter.py; the Claude API supports PNG, JPEG, GIF, and WebP for vision. SVGs (XML text) should be handled via the text path instead, not the vision path.
Applied to files:
autogpt_platform/backend/backend/copilot/sdk/service.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py
📚 Learning: 2026-03-04T12:19:39.243Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12279
File: autogpt_platform/backend/backend/copilot/tools/base.py:184-188
Timestamp: 2026-03-04T12:19:39.243Z
Learning: In autogpt_platform/backend/backend/copilot/tools/, ensure that anonymous users always pass user_id=None to tool execution methods. The anon_ prefix (e.g., anon_123) is used only for PostHog/analytics distinct_id and must not be used as an actual user_id. Use a simple truthiness check on user_id (e.g., if user_id: ... else: ... or a dedicated is_authenticated flag) to distinguish anonymous from authenticated users, and review all tool execution call sites within this directory to prevent accidentally forwarding an anon_ user_id to tools.
Applied to files:
autogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py
- Add 10s timeout via asyncio.wait_for in pause_sandbox() - Catch BaseException instead of Exception in service.py finally block so CancelledError (BaseException, not Exception) is properly handled
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
autogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py (1)
126-136:⚠️ Potential issue | 🟠 MajorKeep the Redis lookup alive if
timeoutno longer kills the sandbox.With
lifecycle={"on_timeout": "pause"}, the sandbox pauses at the timeout but remains indefinitely in E2B (not auto-deleted). Line 136 still expires the Redis key at the same timeout moment:await redis.setex(redis_key, timeout, sandbox.sandbox_id). After that expiry,kill_sandbox()and the session-delete path can no longer find the still-paused sandbox, so abandoned sessions will strand paused sandboxes that E2B retains indefinitely but the application cannot access. Either extend the Redis TTL beyond the sandbox timeout, add an explicit reaper for paused sandboxes, or reverton_timeoutto"kill".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@autogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py` around lines 126 - 136, The Redis key is set to expire at the same timeout even though AsyncSandbox.create is called with lifecycle={"on_timeout": "pause"}, which leaves a paused sandbox in E2B after the Redis entry expires and becomes unreachable; update the logic around AsyncSandbox.create/redis.setex so that if lifecycle.get("on_timeout") != "kill" (or specifically equals "pause") you extend or skip the TTL (e.g., set a much longer TTL or use a non-expiring key) for redis_key when storing sandbox.sandbox_id, otherwise keep the original timeout behavior; locate the AsyncSandbox.create call and the subsequent await redis.setex(redis_key, timeout, sandbox.sandbox_id) and change the TTL calculation/operation accordingly (or add a reaper path) so paused sandboxes remain discoverable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@autogpt_platform/backend/backend/copilot/sdk/service.py`:
- Around line 1465-1472: The pause_sandbox call is currently executed after a
potentially long-running asyncio.shield(upload_transcript(...)) when
claude_agent_use_resume is enabled, which delays stopping billing; move or
decouple the call so pause_sandbox(session_id=..., api_key=config.e2b_api_key)
executes before starting the no-timeout transcript upload (or run it
concurrently and await its completion separately) — locate the block that calls
asyncio.shield(upload_transcript(...)) (and the feature flag
claude_agent_use_resume) and ensure pause_sandbox is invoked immediately prior
to beginning the upload path (or scheduled as an independent task) so billing is
paused at turn end rather than after the upload finishes.
---
Outside diff comments:
In `@autogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py`:
- Around line 126-136: The Redis key is set to expire at the same timeout even
though AsyncSandbox.create is called with lifecycle={"on_timeout": "pause"},
which leaves a paused sandbox in E2B after the Redis entry expires and becomes
unreachable; update the logic around AsyncSandbox.create/redis.setex so that if
lifecycle.get("on_timeout") != "kill" (or specifically equals "pause") you
extend or skip the TTL (e.g., set a much longer TTL or use a non-expiring key)
for redis_key when storing sandbox.sandbox_id, otherwise keep the original
timeout behavior; locate the AsyncSandbox.create call and the subsequent await
redis.setex(redis_key, timeout, sandbox.sandbox_id) and change the TTL
calculation/operation accordingly (or add a reaper path) so paused sandboxes
remain discoverable.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 238c58d7-e7a7-4990-b109-df4271208cc1
⛔ Files ignored due to path filters (1)
autogpt_platform/backend/poetry.lockis excluded by!**/*.lock
📒 Files selected for processing (2)
autogpt_platform/backend/backend/copilot/sdk/service.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
- GitHub Check: types
- GitHub Check: test (3.12)
- GitHub Check: test (3.13)
- GitHub Check: test (3.11)
- GitHub Check: Seer Code Review
- GitHub Check: Check PR Status
🧰 Additional context used
📓 Path-based instructions (4)
autogpt_platform/backend/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
autogpt_platform/backend/**/*.py: Use Python 3.11 (required; managed by Poetry via pyproject.toml) for backend development
Always run 'poetry run format' (Black + isort) before linting in backend development
Always run 'poetry run lint' (ruff) after formatting in backend development
Files:
autogpt_platform/backend/backend/copilot/sdk/service.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py
autogpt_platform/backend/**/*.{py,txt}
📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)
Use
poetry runprefix for all Python commands, including testing, linting, formatting, and migrations
Files:
autogpt_platform/backend/backend/copilot/sdk/service.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py
autogpt_platform/backend/backend/**/*.py
📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)
Use Prisma ORM for database operations in PostgreSQL with pgvector for embeddings
Files:
autogpt_platform/backend/backend/copilot/sdk/service.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py
autogpt_platform/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
Format Python code with
poetry run format
Files:
autogpt_platform/backend/backend/copilot/sdk/service.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py
🧠 Learnings (4)
📚 Learning: 2026-02-26T17:02:22.448Z
Learnt from: Pwuts
Repo: Significant-Gravitas/AutoGPT PR: 12211
File: .pre-commit-config.yaml:160-179
Timestamp: 2026-02-26T17:02:22.448Z
Learning: Keep the pre-commit hook pattern broad for autogpt_platform/backend to ensure OpenAPI schema changes are captured. Do not narrow to backend/api/ alone, since the generated schema depends on Pydantic models across multiple directories (backend/data/, backend/blocks/, backend/copilot/, backend/integrations/, backend/util/). Narrowing could miss schema changes and cause frontend type desynchronization.
Applied to files:
autogpt_platform/backend/backend/copilot/sdk/service.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py
📚 Learning: 2026-03-04T08:04:35.881Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12273
File: autogpt_platform/backend/backend/copilot/tools/workspace_files.py:216-220
Timestamp: 2026-03-04T08:04:35.881Z
Learning: In the AutoGPT Copilot backend, ensure that SVG images are not treated as vision image types by excluding 'image/svg+xml' from INLINEABLE_MIME_TYPES and MULTIMODAL_TYPES in tool_adapter.py; the Claude API supports PNG, JPEG, GIF, and WebP for vision. SVGs (XML text) should be handled via the text path instead, not the vision path.
Applied to files:
autogpt_platform/backend/backend/copilot/sdk/service.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py
📚 Learning: 2026-03-05T15:42:08.207Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 12297
File: .claude/skills/backend-check/SKILL.md:14-16
Timestamp: 2026-03-05T15:42:08.207Z
Learning: In Python files under autogpt_platform/backend (recursively), rely on poetry run format to perform formatting (Black + isort) and linting (ruff). Do not run poetry run lint as a separate step after poetry run format, since format already includes linting checks.
Applied to files:
autogpt_platform/backend/backend/copilot/sdk/service.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py
📚 Learning: 2026-03-04T12:19:39.243Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12279
File: autogpt_platform/backend/backend/copilot/tools/base.py:184-188
Timestamp: 2026-03-04T12:19:39.243Z
Learning: In autogpt_platform/backend/backend/copilot/tools/, ensure that anonymous users always pass user_id=None to tool execution methods. The anon_ prefix (e.g., anon_123) is used only for PostHog/analytics distinct_id and must not be used as an actual user_id. Use a simple truthiness check on user_id (e.g., if user_id: ... else: ... or a dedicated is_authenticated flag) to distinguish anonymous from authenticated users, and review all tool execution call sites within this directory to prevent accidentally forwarding an anon_ user_id to tools.
Applied to files:
autogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py
- Split `timeout` param into `sandbox_timeout` (4h, e2b auto-pause) and `redis_ttl` (48h, allow reconnection to paused sandboxes) - Add test for pause→reconnect cycle (addresses Sentry race-condition comment)
|
Addressing Sentry comment #2901401483: added a dedicated test for the pause→reconnect cycle ( |
…inally Pause must happen before asyncio.shield(upload_transcript) which has no timeout, otherwise billing continues while the upload is in progress.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
autogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py (1)
204-242: Minor: Consider race between pause and kill operations.When a session is deleted while a turn is ending,
pause_sandboxandkill_sandboxcan race on the same sandbox. The current implementation handles this gracefully—both operations are best-effort with exception handling, and the worst case is a paused sandbox that times out naturally.For additional robustness, consider:
- Using a short Redis lock during pause/kill operations
- Having kill_sandbox check if pause is in progress
Given the best-effort semantics and the auto-pause timeout safety net, this is acceptable as-is.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@autogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py` around lines 204 - 242, Kill_sandbox can race with pause_sandbox; add a short Redis-based lock around the pause/kill sequence to serialize operations: in kill_sandbox (and in pause_sandbox) acquire a short-lived lock key based on _SANDBOX_REDIS_PREFIX+session_id (use a SETNX-like acquire with a small TTL), skip or retry the operation if lock cannot be obtained, and release the lock when done; additionally, have kill_sandbox check the Redis value after acquiring the lock (using the same redis_key read and compare against _CREATING or a new PAUSING marker) to avoid killing a sandbox that pause_sandbox just marked for pause.autogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.py (1)
288-358: LGTM!Excellent test coverage for
pause_sandbox:
- Happy path with successful pause
- Edge cases: no sandbox, creating state, connection failure
- The
test_pause_then_reconnect_reuses_sandboxtest validates the critical pause→resume cycleConsider adding a test for timeout behavior (when
asyncio.wait_fortimes out) for completeness, similar totest_kill_timeout_returns_false.🧪 Optional: Add timeout test for pause_sandbox
+ def test_pause_timeout_returns_false(self): + """Returns False when E2B API calls exceed the 10s timeout.""" + redis = _mock_redis(get_val="sb-abc") + with ( + _patch_redis(redis), + patch( + "backend.copilot.tools.e2b_sandbox.asyncio.wait_for", + new_callable=AsyncMock, + side_effect=asyncio.TimeoutError, + ), + ): + result = asyncio.run(pause_sandbox("sess-123", _API_KEY)) + + assert result is False🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@autogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.py` around lines 288 - 358, Add a new unit test that asserts pause_sandbox returns False when the pause operation times out: create a sandbox via _mock_sandbox and a redis mock (like in other tests), patch backend.copilot.tools.e2b_sandbox.AsyncSandbox.connect to return that sandbox, then simulate a timeout (either by making the sandbox.pause AsyncMock raise asyncio.TimeoutError or by patching asyncio.wait_for to raise TimeoutError) and call pause_sandbox("sess-123", _API_KEY); verify the function returns False and behaves like test_kill_timeout_returns_false. Reference pause_sandbox, AsyncSandbox, _mock_sandbox, and test_kill_timeout_returns_false when locating code to mirror.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@autogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py`:
- Around line 143-148: The sandbox creation call uses an invalid lifecycle
parameter; replace the AsyncSandbox.create(...) call with
AsyncSandbox.beta_create(...) and pass auto_pause=True along with the existing
timeout and api_key (e.g., AsyncSandbox.beta_create(auto_pause=True,
timeout=sandbox_timeout, api_key=api_key, template=template)) so the sandbox is
paused on timeout; update the call site where AsyncSandbox.create is used to
AsyncSandbox.beta_create and remove lifecycle={"on_timeout": "pause"}.
---
Nitpick comments:
In `@autogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.py`:
- Around line 288-358: Add a new unit test that asserts pause_sandbox returns
False when the pause operation times out: create a sandbox via _mock_sandbox and
a redis mock (like in other tests), patch
backend.copilot.tools.e2b_sandbox.AsyncSandbox.connect to return that sandbox,
then simulate a timeout (either by making the sandbox.pause AsyncMock raise
asyncio.TimeoutError or by patching asyncio.wait_for to raise TimeoutError) and
call pause_sandbox("sess-123", _API_KEY); verify the function returns False and
behaves like test_kill_timeout_returns_false. Reference pause_sandbox,
AsyncSandbox, _mock_sandbox, and test_kill_timeout_returns_false when locating
code to mirror.
In `@autogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py`:
- Around line 204-242: Kill_sandbox can race with pause_sandbox; add a short
Redis-based lock around the pause/kill sequence to serialize operations: in
kill_sandbox (and in pause_sandbox) acquire a short-lived lock key based on
_SANDBOX_REDIS_PREFIX+session_id (use a SETNX-like acquire with a small TTL),
skip or retry the operation if lock cannot be obtained, and release the lock
when done; additionally, have kill_sandbox check the Redis value after acquiring
the lock (using the same redis_key read and compare against _CREATING or a new
PAUSING marker) to avoid killing a sandbox that pause_sandbox just marked for
pause.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 58dcde3a-33d3-41ac-90f1-d12fff495e47
📒 Files selected for processing (3)
autogpt_platform/backend/backend/copilot/sdk/service.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
- GitHub Check: types
- GitHub Check: Seer Code Review
- GitHub Check: Check PR Status
- GitHub Check: test (3.11)
- GitHub Check: test (3.13)
- GitHub Check: test (3.12)
🧰 Additional context used
📓 Path-based instructions (6)
autogpt_platform/backend/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
autogpt_platform/backend/**/*.py: Use Python 3.11 (required; managed by Poetry via pyproject.toml) for backend development
Always run 'poetry run format' (Black + isort) before linting in backend development
Always run 'poetry run lint' (ruff) after formatting in backend development
Files:
autogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.pyautogpt_platform/backend/backend/copilot/sdk/service.py
autogpt_platform/backend/**/*.{py,txt}
📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)
Use
poetry runprefix for all Python commands, including testing, linting, formatting, and migrations
Files:
autogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.pyautogpt_platform/backend/backend/copilot/sdk/service.py
autogpt_platform/backend/**/*_test.py
📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)
autogpt_platform/backend/**/*_test.py: Always review snapshot changes withgit diffbefore committing when updating snapshots withpoetry run pytest --snapshot-update
Use pytest with snapshot testing for API responses in test files
Colocate test files with source files using the*_test.pynaming convention
Files:
autogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.py
autogpt_platform/backend/backend/**/*.py
📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)
Use Prisma ORM for database operations in PostgreSQL with pgvector for embeddings
Files:
autogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.pyautogpt_platform/backend/backend/copilot/sdk/service.py
autogpt_platform/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
Format Python code with
poetry run format
Files:
autogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.pyautogpt_platform/backend/backend/copilot/sdk/service.py
autogpt_platform/backend/**/*test*.py
📄 CodeRabbit inference engine (AGENTS.md)
Run
poetry run testfor backend testing (runs pytest with docker based postgres + prisma)
Files:
autogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.py
🧠 Learnings (4)
📚 Learning: 2026-02-26T17:02:22.448Z
Learnt from: Pwuts
Repo: Significant-Gravitas/AutoGPT PR: 12211
File: .pre-commit-config.yaml:160-179
Timestamp: 2026-02-26T17:02:22.448Z
Learning: Keep the pre-commit hook pattern broad for autogpt_platform/backend to ensure OpenAPI schema changes are captured. Do not narrow to backend/api/ alone, since the generated schema depends on Pydantic models across multiple directories (backend/data/, backend/blocks/, backend/copilot/, backend/integrations/, backend/util/). Narrowing could miss schema changes and cause frontend type desynchronization.
Applied to files:
autogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.pyautogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-03-04T08:04:35.881Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12273
File: autogpt_platform/backend/backend/copilot/tools/workspace_files.py:216-220
Timestamp: 2026-03-04T08:04:35.881Z
Learning: In the AutoGPT Copilot backend, ensure that SVG images are not treated as vision image types by excluding 'image/svg+xml' from INLINEABLE_MIME_TYPES and MULTIMODAL_TYPES in tool_adapter.py; the Claude API supports PNG, JPEG, GIF, and WebP for vision. SVGs (XML text) should be handled via the text path instead, not the vision path.
Applied to files:
autogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.pyautogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-03-04T12:19:39.243Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12279
File: autogpt_platform/backend/backend/copilot/tools/base.py:184-188
Timestamp: 2026-03-04T12:19:39.243Z
Learning: In autogpt_platform/backend/backend/copilot/tools/, ensure that anonymous users always pass user_id=None to tool execution methods. The anon_ prefix (e.g., anon_123) is used only for PostHog/analytics distinct_id and must not be used as an actual user_id. Use a simple truthiness check on user_id (e.g., if user_id: ... else: ... or a dedicated is_authenticated flag) to distinguish anonymous from authenticated users, and review all tool execution call sites within this directory to prevent accidentally forwarding an anon_ user_id to tools.
Applied to files:
autogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py
📚 Learning: 2026-03-05T15:42:08.207Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 12297
File: .claude/skills/backend-check/SKILL.md:14-16
Timestamp: 2026-03-05T15:42:08.207Z
Learning: In Python files under autogpt_platform/backend (recursively), rely on poetry run format to perform formatting (Black + isort) and linting (ruff). Do not run poetry run lint as a separate step after poetry run format, since format already includes linting checks.
Applied to files:
autogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.pyautogpt_platform/backend/backend/copilot/sdk/service.py
🔇 Additional comments (9)
autogpt_platform/backend/backend/copilot/sdk/service.py (3)
63-63: LGTM!The import correctly adds
pause_sandboxalongsideget_or_create_sandboxfrom the e2b_sandbox module.
796-801: LGTM!The call to
get_or_create_sandboxcorrectly uses the renamedsandbox_timeoutparameter, aligning with the updated function signature.
1465-1472: Pause implementation is correct; ordering concern already flagged.The
pause_sandboxcall correctly:
- Checks config flags before attempting pause
- Handles all exceptions including
BaseException- Logs failures as warnings without breaking the finally flow
The concern about pause occurring after the potentially unbounded transcript upload was raised in a previous review comment. The current ordering may be intentional to ensure transcript integrity before pausing.
autogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py (4)
9-26: LGTM!The updated docstring clearly documents the sandbox lifecycle stages and cost control mechanism. The relationship between per-turn pause (primary) and on_timeout pause (safety net) is well explained.
43-49: LGTM!The constants are well-documented and the values are sensible:
- 4-hour sandbox timeout as a safety net for runaway sessions
- 48-hour Redis TTL ensures paused sandboxes remain reconnectable across sessions
71-89: LGTM!The updated signature with
sandbox_timeoutandredis_ttlparameters is well-documented. The docstring clearly explains the purpose of each parameter and their relationship.
162-201: LGTM!The
pause_sandboxfunction is well-implemented:
- Returns early for missing/creating sandboxes (lines 174-179)
- Uses
asyncio.wait_forwith a 10-second timeout (line 187) to prevent indefinite blocking- Catches all exceptions and returns
Falsegracefully (lines 194-201)- Properly logs outcomes for debugging
The timeout addresses the concern raised in the previous review about unbounded E2B API calls.
autogpt_platform/backend/backend/copilot/tools/e2b_sandbox_test.py (2)
13-20: LGTM!The import statement correctly includes
pause_sandboxalongside existing imports.
137-139: LGTM!Good test coverage for the new
lifecycleparameter. The assertion verifies that sandbox creation correctly passes{"on_timeout": "pause"}to the E2B SDK.
|
Addressing r2901435324: |
- Remove unnecessary # type: ignore on sandbox.run_code (e2b v2 types are correct) - Add config.e2b_active property to centralize use_e2b_sandbox && e2b_api_key check - Update e2b_sandbox_timeout default from 12h to 4h in config - Extract _act_on_sandbox() helper to eliminate duplicate code in pause/kill - Fix single 10s timeout wrapping both connect+action (was accidentally split into 2x10s)
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@autogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py`:
- Around line 144-149: The code currently calls AsyncSandbox.create(...,
lifecycle={"on_timeout": "pause"}, timeout=sandbox_timeout) which is unsupported
in e2b SDK v2; replace this call with AsyncSandbox.beta_create(...,
auto_pause=True, timeout=sandbox_timeout) and pass the same template and api_key
values (i.e., use AsyncSandbox.beta_create with template, api_key,
auto_pause=True and timeout set to sandbox_timeout) so the sandbox will pause on
timeout instead of failing to initialize.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: dc9e5b73-dc86-4962-b87a-282d38676b1c
📒 Files selected for processing (3)
autogpt_platform/backend/backend/copilot/config.pyautogpt_platform/backend/backend/copilot/sdk/service.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
- GitHub Check: types
- GitHub Check: Seer Code Review
- GitHub Check: test (3.12)
- GitHub Check: test (3.11)
- GitHub Check: test (3.13)
- GitHub Check: Check PR Status
🧰 Additional context used
📓 Path-based instructions (4)
autogpt_platform/backend/**/*.py
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
autogpt_platform/backend/**/*.py: Use Python 3.11 (required; managed by Poetry via pyproject.toml) for backend development
Always run 'poetry run format' (Black + isort) before linting in backend development
Always run 'poetry run lint' (ruff) after formatting in backend development
Files:
autogpt_platform/backend/backend/copilot/config.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.pyautogpt_platform/backend/backend/copilot/sdk/service.py
autogpt_platform/backend/**/*.{py,txt}
📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)
Use
poetry runprefix for all Python commands, including testing, linting, formatting, and migrations
Files:
autogpt_platform/backend/backend/copilot/config.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.pyautogpt_platform/backend/backend/copilot/sdk/service.py
autogpt_platform/backend/backend/**/*.py
📄 CodeRabbit inference engine (autogpt_platform/backend/CLAUDE.md)
Use Prisma ORM for database operations in PostgreSQL with pgvector for embeddings
Files:
autogpt_platform/backend/backend/copilot/config.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.pyautogpt_platform/backend/backend/copilot/sdk/service.py
autogpt_platform/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
Format Python code with
poetry run format
Files:
autogpt_platform/backend/backend/copilot/config.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.pyautogpt_platform/backend/backend/copilot/sdk/service.py
🧠 Learnings (6)
📓 Common learnings
Learnt from: Pwuts
Repo: Significant-Gravitas/AutoGPT PR: 12284
File: autogpt_platform/frontend/src/app/api/openapi.json:11897-11900
Timestamp: 2026-03-04T23:58:09.319Z
Learning: Repo: Significant-Gravitas/AutoGPT — PR `#12284`
Backend/frontend OpenAPI codegen convention: In backend/api/features/store/model.py, the StoreSubmission and StoreSubmissionAdminView models define submitted_at: datetime | None, changes_summary: str | None, and instructions: str | None with no default. This is intentional to produce “required but nullable” fields in OpenAPI (properties appear in required[] and use anyOf [type, null]). This matches Prisma’s submittedAt DateTime? and changesSummary String?. Do not flag this as a required/nullable mismatch.
📚 Learning: 2026-03-04T23:58:09.319Z
Learnt from: Pwuts
Repo: Significant-Gravitas/AutoGPT PR: 12284
File: autogpt_platform/frontend/src/app/api/openapi.json:11897-11900
Timestamp: 2026-03-04T23:58:09.319Z
Learning: Repo: Significant-Gravitas/AutoGPT — PR `#12284`
Backend/frontend OpenAPI codegen convention: In backend/api/features/store/model.py, the StoreSubmission and StoreSubmissionAdminView models define submitted_at: datetime | None, changes_summary: str | None, and instructions: str | None with no default. This is intentional to produce “required but nullable” fields in OpenAPI (properties appear in required[] and use anyOf [type, null]). This matches Prisma’s submittedAt DateTime? and changesSummary String?. Do not flag this as a required/nullable mismatch.
Applied to files:
autogpt_platform/backend/backend/copilot/config.py
📚 Learning: 2026-02-26T17:02:22.448Z
Learnt from: Pwuts
Repo: Significant-Gravitas/AutoGPT PR: 12211
File: .pre-commit-config.yaml:160-179
Timestamp: 2026-02-26T17:02:22.448Z
Learning: Keep the pre-commit hook pattern broad for autogpt_platform/backend to ensure OpenAPI schema changes are captured. Do not narrow to backend/api/ alone, since the generated schema depends on Pydantic models across multiple directories (backend/data/, backend/blocks/, backend/copilot/, backend/integrations/, backend/util/). Narrowing could miss schema changes and cause frontend type desynchronization.
Applied to files:
autogpt_platform/backend/backend/copilot/config.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.pyautogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-03-04T08:04:35.881Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12273
File: autogpt_platform/backend/backend/copilot/tools/workspace_files.py:216-220
Timestamp: 2026-03-04T08:04:35.881Z
Learning: In the AutoGPT Copilot backend, ensure that SVG images are not treated as vision image types by excluding 'image/svg+xml' from INLINEABLE_MIME_TYPES and MULTIMODAL_TYPES in tool_adapter.py; the Claude API supports PNG, JPEG, GIF, and WebP for vision. SVGs (XML text) should be handled via the text path instead, not the vision path.
Applied to files:
autogpt_platform/backend/backend/copilot/config.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.pyautogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-03-05T15:42:08.207Z
Learnt from: ntindle
Repo: Significant-Gravitas/AutoGPT PR: 12297
File: .claude/skills/backend-check/SKILL.md:14-16
Timestamp: 2026-03-05T15:42:08.207Z
Learning: In Python files under autogpt_platform/backend (recursively), rely on poetry run format to perform formatting (Black + isort) and linting (ruff). Do not run poetry run lint as a separate step after poetry run format, since format already includes linting checks.
Applied to files:
autogpt_platform/backend/backend/copilot/config.pyautogpt_platform/backend/backend/copilot/tools/e2b_sandbox.pyautogpt_platform/backend/backend/copilot/sdk/service.py
📚 Learning: 2026-03-04T12:19:39.243Z
Learnt from: majdyz
Repo: Significant-Gravitas/AutoGPT PR: 12279
File: autogpt_platform/backend/backend/copilot/tools/base.py:184-188
Timestamp: 2026-03-04T12:19:39.243Z
Learning: In autogpt_platform/backend/backend/copilot/tools/, ensure that anonymous users always pass user_id=None to tool execution methods. The anon_ prefix (e.g., anon_123) is used only for PostHog/analytics distinct_id and must not be used as an actual user_id. Use a simple truthiness check on user_id (e.g., if user_id: ... else: ... or a dedicated is_authenticated flag) to distinguish anonymous from authenticated users, and review all tool execution call sites within this directory to prevent accidentally forwarding an anon_ user_id to tools.
Applied to files:
autogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py
🔇 Additional comments (8)
autogpt_platform/backend/backend/copilot/config.py (1)
114-123: LGTM! Centralized E2B activation logic is clean.The new
e2b_activeproperty correctly encapsulates the two-field check (use_e2b_sandboxandbool(e2b_api_key)) into a single source of truth. The timeout reduction from 12h to 4h aligns with the per-turn pause strategy described in the PR.Note: The relevant code snippet from
routes.py:269still uses the oldconfig.use_e2b_sandbox and config.e2b_api_keypattern instead ofconfig.e2b_active. Consider updating that call site for consistency (outside this file's scope).autogpt_platform/backend/backend/copilot/sdk/service.py (3)
63-63: LGTM!Import correctly updated to include the new
pause_sandboxfunction.
794-800: LGTM!Clean migration to the centralized
e2b_activeproperty and correctly passingsandbox_timeouttoget_or_create_sandbox.
1419-1429: LGTM! Pause placement correctly addresses billing concerns.The pause is now executed BEFORE the transcript upload (which has no timeout via
asyncio.shield), ensuring billing stops at turn end regardless of upload duration. This correctly addresses the previous review concern.One minor observation:
pause_sandboxreturnsbooland swallows exceptions internally via_act_on_sandbox, so theexcept BaseExceptionhere will typically not catch anything frompause_sandboxitself. However, keeping the try-except is harmless and defensive.autogpt_platform/backend/backend/copilot/tools/e2b_sandbox.py (4)
1-26: LGTM!Clear and comprehensive module docstring explaining the lifecycle: turn start (connect/auto-resume), execution, turn end (pause), and session delete (kill). Cost control section explains the safety-net behavior of
on_timeout: pause.
44-50: LGTM!Good separation of concerns:
_E2B_SANDBOX_TIMEOUT(4h) for e2b running time before auto-pause, and_REDIS_SANDBOX_TTL(48h) for Redis key retention. The longer Redis TTL ensures paused sandboxes remain reconnectable across the session lifetime.
163-213: LGTM! Timeout added to connect+action path.The
_act_on_sandboxhelper correctly addresses the previous review concern by wrapping the connect and action calls inasyncio.wait_for(..., timeout=10). This prevents indefinite hangs in the finally block.The consolidated error handling and optional Redis cleanup (
delete_redisflag) cleanly separates pause (no Redis delete) from kill (Redis delete) behavior.
216-236: LGTM!Clean public API for
pause_sandboxandkill_sandboxusing the shared_act_on_sandboxhelper. The lambda approach for passing the async method is appropriate.
Remove standalone _REDIS_SANDBOX_TTL constant; pass session_ttl from service.py so the sandbox Redis key expires with the session lifecycle.
…data
Reverts the ChatSession.metadata DB column approach in favour of a simpler
Redis-based design:
- sandbox_id stored at copilot:e2b:sandbox:{session_id} (30-day TTL)
- Redis creation lock (SET NX) still used to serialise concurrent creates
- lifecycle={"on_timeout": "pause"} with 1-hour timeout as safety net for
a missed explicit pause; paused sandboxes are free and cleaned up by the
E2B project-level "paused sandbox lifetime" setting — no scheduler needed
- pause_sandbox() is now fire-and-forget (asyncio.create_task) in service.py
so it does not block the response or the transcript upload
- kill_sandbox() clears the Redis key on success; on failure the key stays
so the kill can be retried
- Removes: ChatSessionMetadata model, schema.prisma metadata column,
migration, 4 DB functions in db.py + db_manager.py, scheduler cleanup job
- No-op migration SQL left in place so Prisma history stays intact
- Remove separate _CREATION_LOCK_PREFIX key; the sandbox key now doubles as a creation lock via a "creating" sentinel value (SET NX) - Add on_timeout param to get_or_create_sandbox (Literal["kill","pause"]) wired to new ChatConfig.e2b_sandbox_on_timeout field (default: "pause") - Rename pause_timeout -> timeout in get_or_create_sandbox signature to match the E2B SDK param name - Fix TTLs: _E2B_TIMEOUT=1h (was 4h in config), _SANDBOX_ID_TTL=48h (was 30d) - Update tests: sentinel-based waiting, on_timeout coverage, sentinel no-op tests for kill/pause
- Update module docstring: on_timeout is now configurable, not hardcoded - Simplify _act_on_sandbox inner coroutine (_connect_and_act -> _run, one-liner)
…ut to 3h E2B timeout is wall-clock (not idle-based), so 1h was too conservative for long-running agent turns. Raise to 3h to match the 48h Redis key TTL and give sessions room to breathe between explicit per-turn pauses. Also drops the empty no-op migration that was left as a placeholder.
… type hints - _try_reconnect now calls _set_stored_sandbox_id after a successful reconnect to reset the 48h TTL, preventing active sessions from losing their sandbox key right before expiry - Fix docstring: "default: 1 h" → "default: 3 h" in get_or_create_sandbox - Replace Coroutine (unparameterized) with Awaitable[Any] for _act_on_sandbox fn param - Simplify _setup_e2b: single walrus check instead of two separate if-branches - Update tests: assert redis.set called on reconnect (TTL refresh)
Add pause_sandbox_direct() which takes the already-connected sandbox object and calls sandbox.pause() directly, skipping the Redis lookup and AsyncSandbox.connect() round-trip that pause_sandbox() would make. Use it in service.py turn teardown (fire-and-forget) where e2b_sandbox is already in scope, saving ~100-300ms of E2B HTTP latency per turn. pause_sandbox() is kept for callers that only have session_id (e.g. external cleanup jobs). Add 3 tests for pause_sandbox_direct (success, failure, timeout).
autogpt-reviewer
left a comment
There was a problem hiding this comment.
PR #12330 — feat(copilot): E2B sandbox auto-pause between turns to eliminate idle billing
Author: majdyz (Zamil Majdy) | Files: 8 changed (+591/−206) | CI: ✅ All green
🎯 Verdict: APPROVE_WITH_CONDITIONS
What This PR Does
Introduces per-turn sandbox pausing for E2B sandboxes in the CoPilot, eliminating idle compute billing between user turns. Paused sandboxes cost $0 and auto-resume transparently on the next turn via AsyncSandbox.connect(). Also adds explicit sandbox cleanup on session deletion and upgrades the E2B SDK from v1 to v2.
Specialist Findings
🛡️ Security ✅ — No issues. API keys properly handled (never logged/exposed), Redis keys use server-generated UUIDs, error handling doesn't leak sensitive info, resource cleanup is thorough with safety nets.
🏗️ Architecture ✅ — Clean lifecycle abstraction via _act_on_sandbox() helper. Dual-timeout model (4h E2B / 48h Redis) correctly addresses paused-sandbox-outlives-key issue. e2b_active centralizes activation logic. Minor: routes.py:269 still uses old pattern instead of e2b_active; inline import in model.py masks coupling.
⚡ Performance ✅ — Net positive. Eliminates idle billing (primary win). Per-turn pause adds negligible latency (runs after response, in finally block). Resume adds ~1-3s to turn start (acceptable trade-off). All operations bounded by 10s asyncio.wait_for timeout. No N+1 or scaling concerns.
🧪 Testing pause_sandbox (equivalent exists for kill_sandbox). Missing service-layer test verifying pause ordering (pause before transcript upload). Missing e2b_active property unit tests.
📖 Quality ✅ — High quality. Comprehensive module docstring, clear constant naming, proper function docstrings, consistent error handling. Minor nitpick: hardcoded 10 timeout could be a named constant.
📦 Product ✅ — Strong alignment. Significant cost savings (potentially 90%+ reduction in sandbox compute costs). Transparent to users — state preserved across pause/resume. E2B SDK v2 migration is contained.
📬 Discussion ✅ — All substantive discussions resolved. CodeRabbit's concerns about Redis TTL, operation timeouts, pause ordering, and API validity were all addressed by the author. CodeRabbit repeatedly (incorrectly) claimed lifecycle={"on_timeout": "pause"} was invalid — author confirmed it's the stable v2 API (e2b 2.15.1). Routine merge conflicts with dependabot PRs remain.
🔎 QA ✅ — Environment healthy. Frontend loads correctly, backend responds, no regressions. E2B-specific pause/resume cycle requires real API keys (author verified E2E manually with real credentials).
Conditions for Merge
- Add
pause_sandboxtimeout test (e2b_sandbox_test.py) — follows existing pattern fromtest_kill_timeout_returns_false. Easy fix, ensures parity.
Should Fix (Follow-up OK)
routes.py:269— Useconfig.e2b_activeinstead ofconfig.use_e2b_sandbox and config.e2b_api_keyfor consistencye2b_sandbox.py:196— Extract hardcoded10timeout to_E2B_API_TIMEOUT_SECONDSconstantconfig.py— Add unit tests fore2b_activeproperty (3 cases: both true, missing key, disabled)service.py— Add integration test verifying pause-before-transcript-upload ordering
Risk Assessment
Merge risk: LOW | Rollback: EASY (feature-flagged via use_e2b_sandbox + e2b_api_key)
The change is well-contained to the copilot E2B sandbox module. The dual safety nets (explicit per-turn pause + auto-pause on timeout) ensure robustness. The E2B SDK v2 migration only touches the sandbox module and a trivial rename in code_executor.py.
Reviewed by 8 automated specialists: Security, Architecture, Performance, Testing, Quality, Product, Discussion, QA.
autogpt-reviewer
left a comment
There was a problem hiding this comment.
PR #12330 — feat(copilot): E2B sandbox auto-pause between turns to eliminate idle billing
Author: majdyz (Zamil Majdy) | Files: 8 changed (+591/−206) | CI: ✅ All green (tests 3.11/3.12/3.13, types, CodeQL, Snyk)
🎯 Verdict: APPROVE_WITH_CONDITIONS
What This PR Does
Introduces per-turn sandbox pausing for E2B sandboxes in the CoPilot, eliminating idle compute billing between user turns. Paused sandboxes cost $0 and auto-resume transparently on the next turn via AsyncSandbox.connect(). Also adds explicit sandbox cleanup on session deletion and upgrades the E2B SDK from v1 to v2.
Specialist Findings
🛡️ Security ✅ — No issues. API keys properly handled (never logged/exposed), Redis keys use server-generated UUIDs with user-scoped session ownership enforced at DB layer. _CREATING_SENTINEL can't collide with real sandbox IDs (sb-* format). SET NX creation lock prevents duplicate sandbox creation. E2B SDK v2 transitive deps (dockerfile-parse, wcmatch, bracex, rich) have no known CVEs.
🏗️ Architecture ✅ — Clean lifecycle abstraction via _act_on_sandbox() helper. Dual-timeout model (4h E2B auto-pause / 48h Redis TTL) correctly addresses paused-sandbox-outlives-key issue. e2b_active centralizes activation logic. Fire-and-forget asyncio.create_task() pattern for pause is correctly anchored via _background_tasks set. Minor: routes.py:269 still uses old pattern instead of e2b_active; inline import in model.py masks coupling.
⚡ Performance ✅ — Net positive. Eliminates idle billing (primary win — potentially 90%+ cost reduction). Per-turn pause adds negligible latency (runs after response in finally block via fire-and-forget task). Resume adds ~1-3s to turn start (acceptable trade-off). All operations bounded by 10s asyncio.wait_for timeout. No N+1 or scaling concerns.
🧪 Testing pause_sandbox (equivalent exists for kill_sandbox), missing service-layer test verifying pause ordering, missing e2b_active property unit tests.
📖 Quality ✅ — High quality. Comprehensive module docstring documenting 4 lifecycle stages, clear constant naming (_E2B_SANDBOX_TIMEOUT, _REDIS_SANDBOX_TTL), proper function docstrings, consistent error handling with actionable warning-level messages. Walrus operator for type-safe e2b_api_key narrowing is idiomatic.
📦 Product ✅ — Strong alignment. Significant cost savings with transparent user experience — state preserved across pause/resume. E2B SDK v2 migration is contained (sandbox module + trivial code_executor.py rename). Feature-flagged via use_e2b_sandbox + e2b_api_key.
📬 Discussion ✅ — All substantive discussions resolved. Author addressed CodeRabbit concerns about Redis TTL (added separate redis_ttl param), operation timeouts (added asyncio.wait_for), pause ordering (moved before transcript upload), and API validity (confirmed lifecycle={"on_timeout": "pause"} is stable v2 API in e2b 2.15.1, not beta). Routine merge conflicts with dependabot PRs remain.
🔎 QA ✅ — Environment healthy. Frontend loads correctly, backend responds, Storybook renders, no regressions. E2B-specific pause/resume cycle requires real API keys — author verified E2E manually with real credentials.
Conditions for Merge
- Add
pause_sandboxtimeout test (e2b_sandbox_test.py) — follows existing pattern fromtest_kill_timeout_returns_false. Ensures parity in edge case coverage.
Should Fix (Follow-up OK)
routes.py:269— Useconfig.e2b_activeinstead ofconfig.use_e2b_sandbox and config.e2b_api_keyfor consistency with the new centralized propertye2b_sandbox.py— Extract hardcoded10timeout to_E2B_API_TIMEOUT_SECONDSconstantconfig.py— Add unit tests fore2b_activeproperty (3 cases: both true, missing key, disabled)service.py— Add integration test verifying pause-before-transcript-upload ordering- Verify E2B project-level "paused sandbox lifetime" setting aligns with
_SANDBOX_ID_TTL(48h) to prevent orphaned paused sandboxes accumulating
Risk Assessment
Merge risk: LOW | Rollback: EASY (feature-flagged via use_e2b_sandbox + e2b_api_key)
The change is well-contained to the copilot E2B sandbox module. Dual safety nets (explicit per-turn pause + auto-pause on timeout) ensure robustness. The E2B SDK v2 migration only touches the sandbox module and a trivial rename in code_executor.py. Best-effort pause/kill semantics handle race conditions gracefully.
@ntindle Clean, well-designed cost optimization PR. One condition: add the missing pause_sandbox timeout test for parity with kill_sandbox. Otherwise ready to merge.
Reviewed by 8 automated specialists: Security ✅, Architecture ✅, Performance ✅, Testing
…autopause - Extract hardcoded `10` E2B API timeout to `_E2B_API_TIMEOUT_SECONDS` constant - Add `e2b_active` bool property to `ChatConfig` as single source of truth - Update `routes.py` DELETE /sessions to use `cfg.e2b_active` for consistency - Add `test_pause_timeout_returns_false` to `TestPauseSandbox` (parity with kill) - Add `config_test.py` with 3 unit tests for `e2b_active` (both-set, missing-key, disabled) - Add pause-before-transcript ordering test to `service_test.py`
autogpt-reviewer
left a comment
There was a problem hiding this comment.
PR #12330 — feat(copilot): E2B sandbox auto-pause between turns to eliminate idle billing
Author: majdyz (Zamil Majdy) | Files: 10 changed (+687/−203) | CI: ✅ All green (tests 3.11/3.12/3.13, types, CodeQL, Snyk)
Re-review iteration 2 — Delta: 1 commit (8b39c77 → c423b1e)
🎯 Verdict: APPROVE
All conditions from the previous review (APPROVE_WITH_CONDITIONS) have been addressed by commit c423b1e.
What This PR Does
Introduces per-turn sandbox pausing for E2B sandboxes in the CoPilot, eliminating idle compute billing between user turns. Paused sandboxes cost $0 and auto-resume transparently on the next turn via AsyncSandbox.connect(). Also adds explicit sandbox cleanup on session deletion and upgrades the E2B SDK from v1 to v2.
Previous Conditions — All Resolved ✅
| Condition | Status |
|---|---|
Add pause_sandbox timeout test |
✅ test_pause_timeout_returns_false added — verifies False return + Redis entry preserved |
Should-fix: routes.py use e2b_active |
✅ Now uses _e2b_cfg.e2b_active with assert for type narrowing |
| Should-fix: Extract timeout constant | ✅ _E2B_API_TIMEOUT_SECONDS = 10 used consistently |
Should-fix: e2b_active property tests |
✅ 3 tests in new config_test.py (both enabled, missing key, disabled) |
| Should-fix: Pause-before-upload ordering test | ✅ test_pause_task_scheduled_before_transcript_upload added |
Specialist Findings
🛡️ Security ✅ — No issues. API keys properly handled, Redis keys use server-generated UUIDs, assert pattern is type-narrowing only (no security gate — guarded by e2b_active check). E2B SDK v2 transitive deps have no known CVEs.
🏗️ Architecture ✅ — Clean. e2b_active property is a good single-source-of-truth. _act_on_sandbox() helper properly separates pause (keep Redis) vs kill (clear Redis) semantics. assert in routes.py is standard type-narrowing pattern. Minor: active_e2b_api_key property doesn't filter empty strings (inconsistent with e2b_active's bool() check) — non-blocking since callers handle it.
⚡ Performance ✅ — No behavioral change from delta. _E2B_API_TIMEOUT_SECONDS constant extraction is purely cosmetic. Overall profile remains solid: fire-and-forget pause adds zero latency, all E2B API calls bounded by 10s timeout.
🧪 Testing ✅ — Previous condition fully satisfied. test_pause_timeout_returns_false mirrors test_kill_timeout_keeps_redis. Commit goes beyond minimum: adds 12+ new tests covering happy paths, error handling, timeouts, sentinel filtering, pause→resume cycle, lifecycle parameter passthrough, and leak prevention. New config_test.py and service ordering test are well-structured.
📖 Quality ✅ — High quality. Constants properly named, docstrings comprehensive, test methods descriptive. Minor: _e2b_cfg leading underscore on local var is unusual (cosmetic). _E2B_TIMEOUT constant duplicates config.e2b_sandbox_timeout default (both 10800) — consider removing the dead constant in a follow-up.
📦 Product ✅ — Zero functional changes in the delta. All additions are tests and code quality improvements. No user-facing impact.
📬 Discussion ✅ — All conditions from our previous review addressed. Author correctly dismissed repeated CodeRabbit/Sentry false positives about lifecycle={"on_timeout": "pause"} and sb.pause() being invalid (confirmed stable API in e2b SDK 2.15.1). No new human reviewer concerns.
🔎 QA ✅ — Frontend loads correctly, signup works, copilot UI operational, backend healthy. E2B sandbox pause/resume cannot be tested end-to-end without live API keys, but comprehensive unit tests cover all branches.
QA Screenshots
Nice-to-have (Follow-up OK)
config.py:136—active_e2b_api_keyshould useself.e2b_activeguard instead of rawself.use_e2b_sandboxfor consistency (handles empty string edge case)e2b_sandbox.py:61—_E2B_TIMEOUT = 10800duplicatesconfig.e2b_sandbox_timeoutdefault; consider removing dead constantroutes.py:271—_e2b_cfgleading underscore on local variable is non-standard; plaine2b_cfgis more natural- Verify E2B project-level "paused sandbox lifetime" setting aligns with
_SANDBOX_ID_TTL(48h)
Risk Assessment
Merge risk: LOW | Rollback: EASY (feature-flagged via use_e2b_sandbox + e2b_api_key)
The change is well-contained to the copilot E2B sandbox module. Dual safety nets (explicit per-turn pause + auto-pause on timeout) ensure robustness. Best-effort pause/kill semantics handle race conditions gracefully. All previous conditions resolved.
@ntindle Previous conditions met — all clear to merge. Clean cost optimization with comprehensive test coverage.
Reviewed by 8 automated specialists: Security ✅, Architecture ✅, Performance ✅, Testing ✅, Quality ✅, Product ✅, Discussion ✅, QA ✅
…ew comments - active_e2b_api_key: use self.e2b_active guard (handles empty-string edge case) - Remove dead _E2B_TIMEOUT constant (duplicated config.e2b_sandbox_timeout default); make get_or_create_sandbox timeout param required (all callers already pass it) - routes.py: rename _e2b_cfg -> e2b_cfg (no leading underscore on plain local var) - _SANDBOX_ID_TTL comment already documents 48h alignment with E2B paused lifetime
… billing (Significant-Gravitas#12330) ## Summary ### Before - E2B sandboxes ran continuously between CoPilot turns, billing for idle time - Sandbox timeout caused **termination** (kill), losing all session state - No explicit cleanup when sessions were deleted — sandboxes leaked - Single timeout concept with no separation between pause and kill semantics ### After - **Per-turn pause**: `pause_sandbox()` is called in the `finally` block after every CoPilot turn, stopping billing instantly between turns (paused sandboxes cost \$0 compute) - **Auto-pause safety net**: Sandboxes are created with `lifecycle={"on_timeout": "pause"}` (`pause_timeout` = 4h default) so they auto-pause rather than terminate if the explicit pause is missed - **Auto-reconnect**: `AsyncSandbox.connect()` in e2b SDK v2 auto-resumes paused sandboxes transparently — no extra code needed - **Session delete cleanup**: `kill_sandbox()` is now called in `delete_chat_session()` to explicitly terminate sandboxes and free resources - **Two distinct timeouts**: `pause_timeout` (4h, e2b auto-pause) vs `redis_ttl` (12h, session key lifetime) ### Key Changes | File | Change | |------|--------| | `pyproject.toml` | Bump `e2b-code-interpreter` `1.x` → `2.x` | | `e2b_sandbox.py` | Add `pause_sandbox()`, `kill_sandbox()`, `_act_on_sandbox()` helper; `lifecycle={"on_timeout": "pause"}`; separate `pause_timeout` / `redis_ttl` params | | `sdk/service.py` | Call `pause_sandbox()` in `finally` block **before** transcript upload; use walrus operator for type-safe `e2b_api_key` narrowing | | `model.py` | Call `kill_sandbox()` in `delete_chat_session()`; inline import to avoid circular dependency | | `config.py` | Add `e2b_active` property; rename `e2b_sandbox_timeout` default to 4h | | `e2b_sandbox_test.py` | Add `test_pause_then_reconnect_reuses_sandbox` test; update all `sandbox_timeout` → `pause_timeout` | ### Verified E2E - Used real `E2B_API_KEY` from k8s dev cluster to manually verify: sandbox created → paused → `is_running() == False` → reconnected via `connect()` → state preserved → killed ## Test plan - [x] `poetry run pytest backend/copilot/tools/e2b_sandbox_test.py` — all 19 tests pass - [x] CI: test (3.11, 3.12, 3.13), types — all green - [x] E2E verified with real E2B credentials
… billing (Significant-Gravitas#12330) ## Summary ### Before - E2B sandboxes ran continuously between CoPilot turns, billing for idle time - Sandbox timeout caused **termination** (kill), losing all session state - No explicit cleanup when sessions were deleted — sandboxes leaked - Single timeout concept with no separation between pause and kill semantics ### After - **Per-turn pause**: `pause_sandbox()` is called in the `finally` block after every CoPilot turn, stopping billing instantly between turns (paused sandboxes cost \$0 compute) - **Auto-pause safety net**: Sandboxes are created with `lifecycle={"on_timeout": "pause"}` (`pause_timeout` = 4h default) so they auto-pause rather than terminate if the explicit pause is missed - **Auto-reconnect**: `AsyncSandbox.connect()` in e2b SDK v2 auto-resumes paused sandboxes transparently — no extra code needed - **Session delete cleanup**: `kill_sandbox()` is now called in `delete_chat_session()` to explicitly terminate sandboxes and free resources - **Two distinct timeouts**: `pause_timeout` (4h, e2b auto-pause) vs `redis_ttl` (12h, session key lifetime) ### Key Changes | File | Change | |------|--------| | `pyproject.toml` | Bump `e2b-code-interpreter` `1.x` → `2.x` | | `e2b_sandbox.py` | Add `pause_sandbox()`, `kill_sandbox()`, `_act_on_sandbox()` helper; `lifecycle={"on_timeout": "pause"}`; separate `pause_timeout` / `redis_ttl` params | | `sdk/service.py` | Call `pause_sandbox()` in `finally` block **before** transcript upload; use walrus operator for type-safe `e2b_api_key` narrowing | | `model.py` | Call `kill_sandbox()` in `delete_chat_session()`; inline import to avoid circular dependency | | `config.py` | Add `e2b_active` property; rename `e2b_sandbox_timeout` default to 4h | | `e2b_sandbox_test.py` | Add `test_pause_then_reconnect_reuses_sandbox` test; update all `sandbox_timeout` → `pause_timeout` | ### Verified E2E - Used real `E2B_API_KEY` from k8s dev cluster to manually verify: sandbox created → paused → `is_running() == False` → reconnected via `connect()` → state preserved → killed ## Test plan - [x] `poetry run pytest backend/copilot/tools/e2b_sandbox_test.py` — all 19 tests pass - [x] CI: test (3.11, 3.12, 3.13), types — all green - [x] E2E verified with real E2B credentials



Summary
Before
After
pause_sandbox()is called in thefinallyblock after every CoPilot turn, stopping billing instantly between turns (paused sandboxes cost $0 compute)lifecycle={"on_timeout": "pause"}(pause_timeout= 4h default) so they auto-pause rather than terminate if the explicit pause is missedAsyncSandbox.connect()in e2b SDK v2 auto-resumes paused sandboxes transparently — no extra code neededkill_sandbox()is now called indelete_chat_session()to explicitly terminate sandboxes and free resourcespause_timeout(4h, e2b auto-pause) vsredis_ttl(12h, session key lifetime)Key Changes
pyproject.tomle2b-code-interpreter1.x→2.xe2b_sandbox.pypause_sandbox(),kill_sandbox(),_act_on_sandbox()helper;lifecycle={"on_timeout": "pause"}; separatepause_timeout/redis_ttlparamssdk/service.pypause_sandbox()infinallyblock before transcript upload; use walrus operator for type-safee2b_api_keynarrowingmodel.pykill_sandbox()indelete_chat_session(); inline import to avoid circular dependencyconfig.pye2b_activeproperty; renamee2b_sandbox_timeoutdefault to 4he2b_sandbox_test.pytest_pause_then_reconnect_reuses_sandboxtest; update allsandbox_timeout→pause_timeoutVerified E2E
E2B_API_KEYfrom k8s dev cluster to manually verify: sandbox created → paused →is_running() == False→ reconnected viaconnect()→ state preserved → killedTest plan
poetry run pytest backend/copilot/tools/e2b_sandbox_test.py— all 19 tests pass