[feat] Resume agent sessions across the sandbox lifecycle#5197
[feat] Resume agent sessions across the sandbox lifecycle#5197jp-agenta wants to merge 18 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
Brings in #5185 (Pi approval parking over the ACP permission plane). Conflicts in services/runner/src/{engines/sandbox_agent.ts,server.ts} were comment-only: #5185 rewords the parkable-gate doc blocks to cover the Pi ACP plane alongside Claude ACP, and adds piToolSpecsByName / onPiGateAllowed above one of them. Took the incoming wording; verified statically that no executable line from either side was dropped. The S5 sandbox lifecycle (reconnect, park-to-warm) and the Pi gate envelope coexist untouched.
mmabrouk
left a comment
There was a problem hiding this comment.
🤖 The AI agent says:
I found three P1 and two P2 issues. The patch can corrupt active-session liveness, reconnect native state containing incomplete turns, and lose durable continuity updates under overlapping syncs. It can also park failed remote runs and collapse distinct mount names onto one durable mount.
Please address the inline findings before merge.
| "content-type": "application/json", | ||
| authorization: deps.authorization, | ||
| }, | ||
| body: JSON.stringify({ |
There was a problem hiding this comment.
🤖 The AI agent says:
P2: Make durable continuity updates atomic.
With overlapping syncs for the same session, both calls can read the same old row and then replace the entire data object. Whichever PUT finishes last drops the other harness or turn and can regress the durable record. The fire-and-forget callers make this possible even for rapid sequential turns, so a restart may resume an older native session. Serialize these writes or perform a server-side atomic or CAS merge.
There was a problem hiding this comment.
Confirmed and staying open. Two overlapping fire-and-forget syncs both GET the old row and PUT the whole data object; last writer wins and drops the other harness's harness_sessions entry.
The wrinkle is that the state update is a full PUT by design, not a partial patch, so a client-side merge can never be made safe no matter how it is ordered. The fix has to move server-side, which means either a CAS/if-match on the existing endpoint or a purpose-built merge endpoint. That is a design call rather than a patch, so I would rather land it deliberately than bolt it on here.
Your note on writeSandboxId checks out: the DAO's model_fields_set guard means it only overwrites explicitly-set fields, so it does not clobber data. That part is safe today.
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (4)
api/oss/tests/pytest/unit/sessions/test_owner_claim.py (1)
71-96: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftExecute the ownership script in a real Lua-capable Redis test.
This fake branches on script equality and then duplicates the intended behavior, so invalid Lua or Redis-specific regressions still pass. Add at least one integration test using Redis or a Lua-enabled fakeredis instance.
api/oss/tests/pytest/acceptance/session_states/test_harness_sessions_roundtrip.py (1)
8-11: 🎯 Functional Correctness | 🔵 TrivialAcceptance tests shipped unrun.
The module docstring admits these tests were never executed against a live stack. Given the cross-file finding that
SessionStateDatasub-fields not explicitly set get persisted asnull(see DAO'smodel_dump(mode="json")withoutexclude_unset/exclude_none), it's worth actually running these against a live stack before merge to confirm the round-trip/merge assertions hold.Do you want me to help set up a quick local run against the docker-compose dev stack (per the coding guidelines'
run.shscript) to validate these before merge?services/runner/tests/unit/continuation.test.ts (1)
78-103: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExercise the production prompt-selection logic.
This local helper duplicates the implementation, so the suite passes independently of
runTurn. Extract the decision into an exported helper or invokerunTurnwith a minimal fake environment.services/runner/tests/unit/session-ownership.test.ts (1)
148-190: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReturn the advertised owner values before triggering the sentinel.
Both tests throw inside the resolver, so
assertLocalRunnerOwnershipnever receivesundefinedor the same replica ID. Return those values and inject a failingstartSandboxAgentseam afterward to prove the guard accepted them.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 9c581377-e623-4b3f-bb60-41023d0a9cef
⛔ Files ignored due to path filters (19)
services/runner/pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlweb/packages/agenta-api-client/src/generated/api/resources/sessions/client/Client.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SessionRecordIngestRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SignSessionMountCredentialsRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/tools/client/Client.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/tools/client/requests/ListToolCategoriesRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/tools/client/requests/ListToolIntegrationsRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/tools/client/requests/index.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/BuiltinToolConfig.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/CapabilitiesResult.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/GatewayToolConfig.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/HarnessSessionRecord.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionHeartbeatResponseModel.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionState.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionStateData.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionStateUpsertRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/ToolCatalogCategoriesResponse.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/ToolCatalogCategory.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/index.tsis excluded by!**/generated/**
📒 Files selected for processing (81)
.github/workflows/42-railway-build.ymlapi/ee/tests/pytest/conftest.pyapi/ee/tests/pytest/unit/services/test_organizations_oidc_ssrf.pyapi/oss/src/apis/fastapi/sessions/models.pyapi/oss/src/apis/fastapi/sessions/router.pyapi/oss/src/core/mounts/service.pyapi/oss/src/core/sessions/states/dtos.pyapi/oss/src/core/sessions/streams/dtos.pyapi/oss/src/core/sessions/streams/service.pyapi/oss/src/dbs/postgres/sessions/states/dao.pyapi/oss/src/dbs/postgres/sessions/states/mappings.pyapi/oss/src/dbs/redis/sessions/contract.pyapi/oss/src/dbs/redis/sessions/locks.pyapi/oss/tests/pytest/acceptance/session_states/test_harness_sessions_roundtrip.pyapi/oss/tests/pytest/acceptance/session_states/test_session_states_basics.pyapi/oss/tests/pytest/conftest.pyapi/oss/tests/pytest/unit/session_states/test_harness_sessions_mapping.pyapi/oss/tests/pytest/unit/sessions/test_owner_claim.pyapi/oss/tests/pytest/utils/egress.pyapi/pytest.iniclients/python/agenta_client/__init__.pyclients/python/agenta_client/sessions/client.pyclients/python/agenta_client/sessions/raw_client.pyclients/python/agenta_client/tools/client.pyclients/python/agenta_client/tools/raw_client.pyclients/python/agenta_client/tools/types/tool_resolve_request_tools_item.pyclients/python/agenta_client/types/__init__.pyclients/python/agenta_client/types/builtin_tool_config.pyclients/python/agenta_client/types/capabilities_result.pyclients/python/agenta_client/types/gateway_tool_config.pyclients/python/agenta_client/types/harness_session_record.pyclients/python/agenta_client/types/session_heartbeat_response_model.pyclients/python/agenta_client/types/session_state.pyclients/python/agenta_client/types/session_state_data.pyclients/python/agenta_client/types/session_state_upsert_request.pyclients/python/agenta_client/types/tool_catalog_categories_response.pyclients/python/agenta_client/types/tool_catalog_category.pydocs/docs/self-host/02-configuration.mdxdocs/docs/self-host/guides/08-custom-agent-runner-images.mdxdocs/docs/self-host/guides/09-agent-daytona-sandboxes.mdxhosting/docker-compose/ee/docker-compose.gh.local.ymlhosting/docker-compose/ee/env.ee.dev.examplehosting/docker-compose/ee/env.ee.gh.examplehosting/docker-compose/oss/docker-compose.gh.local.ymlhosting/docker-compose/oss/docker-compose.gh.ssl.ymlhosting/docker-compose/oss/env.oss.dev.examplehosting/docker-compose/oss/env.oss.gh.examplehosting/kubernetes/helm/templates/runner-deployment.yamlhosting/railway/oss/scripts/build-and-push-images.shsdks/python/oss/tests/pytest/conftest.pysdks/python/oss/tests/pytest/unit/agents/connections/test_capabilities.pysdks/python/oss/tests/pytest/unit/agents/test_dtos_agent_template.pysdks/python/oss/tests/pytest/unit/test_secrets_manager_custom_provider_ssrf.pysdks/python/oss/tests/pytest/unit/test_utils_net_ssrf.pysdks/python/oss/tests/pytest/utils/test_webhook_ssrf_v0.pysdks/python/pytest.iniservices/runner/docker/Dockerfile.devservices/runner/docker/Dockerfile.ghservices/runner/docker/README.mdservices/runner/package.jsonservices/runner/patches/sandbox-agent@0.4.2.patchservices/runner/src/engines/sandbox_agent.tsservices/runner/src/engines/sandbox_agent/mount.tsservices/runner/src/engines/sandbox_agent/provider.tsservices/runner/src/engines/sandbox_agent/sandbox-reconnect.tsservices/runner/src/engines/sandbox_agent/session-continuity-durable.tsservices/runner/src/engines/sandbox_agent/session-continuity.tsservices/runner/src/engines/sandbox_agent/session-pool.tsservices/runner/src/server.tsservices/runner/src/sessions/alive.tsservices/runner/tests/setup/hermetic-env.tsservices/runner/tests/unit/continuation.test.tsservices/runner/tests/unit/sandbox-agent-mount.test.tsservices/runner/tests/unit/sandbox-agent-provider.test.tsservices/runner/tests/unit/sandbox-lifecycle.test.tsservices/runner/tests/unit/sandbox-reconnect.test.tsservices/runner/tests/unit/session-continuity-durable.test.tsservices/runner/tests/unit/session-continuity.test.tsservices/runner/tests/unit/session-ownership.test.tsservices/runner/tests/unit/session-pool.test.tsservices/runner/vitest.config.ts
💤 Files with no reviewable changes (3)
- clients/python/agenta_client/tools/types/tool_resolve_request_tools_item.py
- clients/python/agenta_client/types/builtin_tool_config.py
- clients/python/agenta_client/types/gateway_tool_config.py
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 11
🧹 Nitpick comments (4)
api/oss/tests/pytest/unit/sessions/test_owner_claim.py (1)
71-96: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftExecute the ownership script in a real Lua-capable Redis test.
This fake branches on script equality and then duplicates the intended behavior, so invalid Lua or Redis-specific regressions still pass. Add at least one integration test using Redis or a Lua-enabled fakeredis instance.
api/oss/tests/pytest/acceptance/session_states/test_harness_sessions_roundtrip.py (1)
8-11: 🎯 Functional Correctness | 🔵 TrivialAcceptance tests shipped unrun.
The module docstring admits these tests were never executed against a live stack. Given the cross-file finding that
SessionStateDatasub-fields not explicitly set get persisted asnull(see DAO'smodel_dump(mode="json")withoutexclude_unset/exclude_none), it's worth actually running these against a live stack before merge to confirm the round-trip/merge assertions hold.Do you want me to help set up a quick local run against the docker-compose dev stack (per the coding guidelines'
run.shscript) to validate these before merge?services/runner/tests/unit/continuation.test.ts (1)
78-103: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExercise the production prompt-selection logic.
This local helper duplicates the implementation, so the suite passes independently of
runTurn. Extract the decision into an exported helper or invokerunTurnwith a minimal fake environment.services/runner/tests/unit/session-ownership.test.ts (1)
148-190: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReturn the advertised owner values before triggering the sentinel.
Both tests throw inside the resolver, so
assertLocalRunnerOwnershipnever receivesundefinedor the same replica ID. Return those values and inject a failingstartSandboxAgentseam afterward to prove the guard accepted them.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 9c581377-e623-4b3f-bb60-41023d0a9cef
⛔ Files ignored due to path filters (19)
services/runner/pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlweb/packages/agenta-api-client/src/generated/api/resources/sessions/client/Client.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SessionRecordIngestRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SignSessionMountCredentialsRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/tools/client/Client.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/tools/client/requests/ListToolCategoriesRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/tools/client/requests/ListToolIntegrationsRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/tools/client/requests/index.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/BuiltinToolConfig.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/CapabilitiesResult.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/GatewayToolConfig.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/HarnessSessionRecord.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionHeartbeatResponseModel.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionState.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionStateData.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionStateUpsertRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/ToolCatalogCategoriesResponse.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/ToolCatalogCategory.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/index.tsis excluded by!**/generated/**
📒 Files selected for processing (81)
.github/workflows/42-railway-build.ymlapi/ee/tests/pytest/conftest.pyapi/ee/tests/pytest/unit/services/test_organizations_oidc_ssrf.pyapi/oss/src/apis/fastapi/sessions/models.pyapi/oss/src/apis/fastapi/sessions/router.pyapi/oss/src/core/mounts/service.pyapi/oss/src/core/sessions/states/dtos.pyapi/oss/src/core/sessions/streams/dtos.pyapi/oss/src/core/sessions/streams/service.pyapi/oss/src/dbs/postgres/sessions/states/dao.pyapi/oss/src/dbs/postgres/sessions/states/mappings.pyapi/oss/src/dbs/redis/sessions/contract.pyapi/oss/src/dbs/redis/sessions/locks.pyapi/oss/tests/pytest/acceptance/session_states/test_harness_sessions_roundtrip.pyapi/oss/tests/pytest/acceptance/session_states/test_session_states_basics.pyapi/oss/tests/pytest/conftest.pyapi/oss/tests/pytest/unit/session_states/test_harness_sessions_mapping.pyapi/oss/tests/pytest/unit/sessions/test_owner_claim.pyapi/oss/tests/pytest/utils/egress.pyapi/pytest.iniclients/python/agenta_client/__init__.pyclients/python/agenta_client/sessions/client.pyclients/python/agenta_client/sessions/raw_client.pyclients/python/agenta_client/tools/client.pyclients/python/agenta_client/tools/raw_client.pyclients/python/agenta_client/tools/types/tool_resolve_request_tools_item.pyclients/python/agenta_client/types/__init__.pyclients/python/agenta_client/types/builtin_tool_config.pyclients/python/agenta_client/types/capabilities_result.pyclients/python/agenta_client/types/gateway_tool_config.pyclients/python/agenta_client/types/harness_session_record.pyclients/python/agenta_client/types/session_heartbeat_response_model.pyclients/python/agenta_client/types/session_state.pyclients/python/agenta_client/types/session_state_data.pyclients/python/agenta_client/types/session_state_upsert_request.pyclients/python/agenta_client/types/tool_catalog_categories_response.pyclients/python/agenta_client/types/tool_catalog_category.pydocs/docs/self-host/02-configuration.mdxdocs/docs/self-host/guides/08-custom-agent-runner-images.mdxdocs/docs/self-host/guides/09-agent-daytona-sandboxes.mdxhosting/docker-compose/ee/docker-compose.gh.local.ymlhosting/docker-compose/ee/env.ee.dev.examplehosting/docker-compose/ee/env.ee.gh.examplehosting/docker-compose/oss/docker-compose.gh.local.ymlhosting/docker-compose/oss/docker-compose.gh.ssl.ymlhosting/docker-compose/oss/env.oss.dev.examplehosting/docker-compose/oss/env.oss.gh.examplehosting/kubernetes/helm/templates/runner-deployment.yamlhosting/railway/oss/scripts/build-and-push-images.shsdks/python/oss/tests/pytest/conftest.pysdks/python/oss/tests/pytest/unit/agents/connections/test_capabilities.pysdks/python/oss/tests/pytest/unit/agents/test_dtos_agent_template.pysdks/python/oss/tests/pytest/unit/test_secrets_manager_custom_provider_ssrf.pysdks/python/oss/tests/pytest/unit/test_utils_net_ssrf.pysdks/python/oss/tests/pytest/utils/test_webhook_ssrf_v0.pysdks/python/pytest.iniservices/runner/docker/Dockerfile.devservices/runner/docker/Dockerfile.ghservices/runner/docker/README.mdservices/runner/package.jsonservices/runner/patches/sandbox-agent@0.4.2.patchservices/runner/src/engines/sandbox_agent.tsservices/runner/src/engines/sandbox_agent/mount.tsservices/runner/src/engines/sandbox_agent/provider.tsservices/runner/src/engines/sandbox_agent/sandbox-reconnect.tsservices/runner/src/engines/sandbox_agent/session-continuity-durable.tsservices/runner/src/engines/sandbox_agent/session-continuity.tsservices/runner/src/engines/sandbox_agent/session-pool.tsservices/runner/src/server.tsservices/runner/src/sessions/alive.tsservices/runner/tests/setup/hermetic-env.tsservices/runner/tests/unit/continuation.test.tsservices/runner/tests/unit/sandbox-agent-mount.test.tsservices/runner/tests/unit/sandbox-agent-provider.test.tsservices/runner/tests/unit/sandbox-lifecycle.test.tsservices/runner/tests/unit/sandbox-reconnect.test.tsservices/runner/tests/unit/session-continuity-durable.test.tsservices/runner/tests/unit/session-continuity.test.tsservices/runner/tests/unit/session-ownership.test.tsservices/runner/tests/unit/session-pool.test.tsservices/runner/vitest.config.ts
💤 Files with no reviewable changes (3)
- clients/python/agenta_client/tools/types/tool_resolve_request_tools_item.py
- clients/python/agenta_client/types/builtin_tool_config.py
- clients/python/agenta_client/types/gateway_tool_config.py
🛑 Comments failed to post (11)
api/oss/src/dbs/postgres/sessions/states/mappings.py (1)
9-15: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Use
is not Noneinstead of truthy check fordbe.data.The DAO serializes with
if upsert.data is not None(explicit None check), but the mapping usesif dbe.data(truthy check). An empty dict{}from legacy data or manual DB edits would be falsy, returningNoneinstead ofSessionStateData()with default fields — a semantic difference downstream code may not expect.🔧 Consistency fix
def dbe_to_dto(dbe: SessionStateDBE) -> SessionState: - data = SessionStateData.model_validate(dbe.data) if dbe.data else None + data = SessionStateData.model_validate(dbe.data) if dbe.data is not None else None return SessionState(📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.def dbe_to_dto(dbe: SessionStateDBE) -> SessionState: data = SessionStateData.model_validate(dbe.data) if dbe.data is not None else None return SessionState( id=dbe.id, project_id=dbe.project_id, session_id=dbe.session_id, data=data,api/oss/tests/pytest/acceptance/session_states/test_harness_sessions_roundtrip.py (1)
16-19: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash # Confirm how the "acceptance" marker gets applied to tests under this directory. rg -n "acceptance" api/oss/tests/pytest/conftest.py api/pytest.ini rg -n "pytest_collection_modifyitems|acceptance" -g '*.py' api/oss/testsRepository: Agenta-AI/agenta
Length of output: 1421
🏁 Script executed:
#!/bin/bash set -euo pipefail # Inspect the test file, nearby conftest files, and pytest config that could apply markers or skips. git ls-files api/oss/tests/pytest/acceptance/session_states/test_harness_sessions_roundtrip.py \ api/oss/tests/pytest/acceptance/conftest.py \ api/oss/tests/pytest/conftest.py \ api/oss/tests/pytest/unit/conftest.py \ api/oss/tests/legacy/conftest.py \ api/pytest.ini for f in \ api/oss/tests/pytest/acceptance/session_states/test_harness_sessions_roundtrip.py \ api/oss/tests/pytest/acceptance/conftest.py \ api/oss/tests/pytest/conftest.py \ api/oss/tests/pytest/unit/conftest.py \ api/oss/tests/legacy/conftest.py \ api/pytest.ini do echo "===== $f =====" [ -f "$f" ] && sed -n '1,220p' "$f" || echo "(missing)" doneRepository: Agenta-AI/agenta
Length of output: 17268
🏁 Script executed:
#!/bin/bash set -euo pipefail # Look for any explicit acceptance markers or path-based marker application. rg -n "`@pytest`\.mark\.acceptance|mark\.acceptance|acceptance" api/oss/tests/pytest -g '*.py' # Inspect the shared API fixtures that the acceptance test relies on. rg -n "def authed_api|authed_api" api/oss/tests -g '*.py'Repository: Agenta-AI/agenta
Length of output: 50373
🏁 Script executed:
#!/bin/bash set -euo pipefail # Inspect the authed/unauthed fixtures and any collection hooks that mention acceptance or env gating. rg -n "def authed_api|def unauthed_api|AGENTA_API_URL|AGENTA_AUTH_KEY|pytest_collection_modifyitems|mark\.acceptance|acceptance" api/oss/tests -g '*.py' -g '!**/__pycache__/**' # Show the fixture definitions if they exist. for f in \ api/oss/tests/utils/api.py \ api/oss/tests/pytest/utils/api.py \ api/oss/tests/utils/env.py \ api/oss/tests/pytest/utils/env.py do [ -f "$f" ] && { echo "===== $f ====="; sed -n '1,220p' "$f"; } doneRepository: Agenta-AI/agenta
Length of output: 4503
🏁 Script executed:
#!/bin/bash set -euo pipefail # Find the actual fixture module(s) and inspect only the relevant sections. fd -a 'api.py$|env.py$' api/oss/tests api/oss -t f # Narrow down to any acceptance-specific collection or skip logic around the test tree. rg -n "pytest_collection_modifyitems|skipif|mark\.skip|mark\.acceptance|AGENTA_API_URL|AGENTA_AUTH_KEY" api/oss/tests/pytest api/oss/tests/utils -g '*.py'Repository: Agenta-AI/agenta
Length of output: 3587
🏁 Script executed:
#!/bin/bash set -euo pipefail python3 - <<'PY' from pathlib import Path # Print only the files likely to define fixtures or collection hooks. candidates = [ Path("api/oss/tests/utils/api.py"), Path("api/oss/tests/utils/env.py"), Path("api/oss/tests/pytest/conftest.py"), Path("api/oss/tests/pytest/acceptance/conftest.py"), Path("api/oss/tests/pytest/unit/conftest.py"), Path("api/oss/tests/legacy/conftest.py"), ] for p in candidates: if p.exists(): print(f"===== {p} =====") text = p.read_text() for i, line in enumerate(text.splitlines(), 1): if any(key in line for key in ["authed_api", "unauthed_api", "AGENTA_API_URL", "AGENTA_AUTH_KEY", "pytest_collection_modifyitems", "acceptance", "skipif", "mark.acceptance"]): print(f"{i}: {line}") PYRepository: Agenta-AI/agenta
Length of output: 511
🏁 Script executed:
#!/bin/bash set -euo pipefail # Inspect the fixture chain that provides cls_account and see whether it already gates on live env. sed -n '1,260p' api/oss/tests/pytest/utils/accounts.py # Also show any direct references from conftest imports to the shared fixtures module. sed -n '1,120p' api/oss/tests/pytest/conftest.pyRepository: Agenta-AI/agenta
Length of output: 2895
Add the
acceptancemarker on this class.pytest.inionly declares the marker; it doesn’t apply it, and there’s no local hook doing so here. Without an explicit marker or collection filter, these tests can still be collected in runs that don’t setAGENTA_API_URL/AGENTA_AUTH_KEY, andag_envwill fail during fixture setup.services/runner/patches/sandbox-agent@0.4.2.patch (1)
93-130: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
sed -n '1,240p' services/runner/patches/sandbox-agent@0.4.2.patch rg -n "killGroup|detached: true|registerProcessCleanup|child.kill|process.kill\\(" services/runner/patches/sandbox-agent@0.4.2.patchRepository: Agenta-AI/agenta
Length of output: 5595
🏁 Script executed:
sed -n '1,240p' dist/providers/local.js rg -n "process.platform|win32|detached: true|killGroup|child.kill\\(" dist/providers/local.js services/runner/patches/sandbox-agent@0.4.2.patchRepository: Agenta-AI/agenta
Length of output: 968
🏁 Script executed:
git ls-files | rg 'sandbox-agent|providers/local|runner|package.json|README|pnpm-workspace.yaml|workspace'Repository: Agenta-AI/agenta
Length of output: 30600
🏁 Script executed:
rg -n "spawnSandboxAgent|killGroup|detached: true|process.kill\\(-child\\.pid|child\\.kill\\(" services/runner/src services/runner/README.md services/runner/package.json services/runner/AGENTS.md services/runner/CLAUDE.mdRepository: Agenta-AI/agenta
Length of output: 233
🏁 Script executed:
sed -n '260,340p' services/runner/src/engines/sandbox_agent/mount.ts rg -n "win32|darwin|linux|process group|detached: true|kill\\(|SIGTERM|SIGKILL" services/runner/src/engines/sandbox_agent services/runner/README.md services/runner/package.json services/runner/AGENTS.md services/runner/CLAUDE.mdRepository: Agenta-AI/agenta
Length of output: 4481
🌐 Web query:
Node.js process.kill negative pid child.kill detached process group Windows documentation💡 Result:
In Node.js, using a negative PID with process.kill to target a process group ID does not work on Windows [1]. The concept of process groups as implemented in POSIX systems (where a negative PID represents a process group) is not supported in the same way on Windows [1]. When you spawn a child process on Windows with the detached: true option, Node.js does not create a process group that can be signaled via a negative PID [1][2]. Consequently, Node.js cannot use process.kill or child.kill to terminate an entire tree of processes on Windows [3]. To terminate a process tree (including detached child processes and their descendants) on Windows, the recommended approach is to use the Windows-native taskkill command, often executed via child_process.exec or a similar mechanism [1][3]. Specifically, the command: taskkill /pid /T /F is commonly used, where /T terminates the process tree (the process and any child processes it started) and /F forces the termination [3]. Third-party libraries like fkill are also frequently used to handle these cross-platform differences automatically [1].
Citations:
- 1: https://github.com/ehmicky/cross-platform-node-guide/blob/main/docs/6_networking_ipc/signals.md
- 2: https://stackoverflow.com/questions/63470352/kill-detached-child-process-in-node
- 3: overengineeringstudio/effect-utils#31
Fall back to
child.kill(signal)when group signaling fails.killGroupswallowsprocess.kill(-pid, ...)errors, and this runner includes awin32-x64build where negative-PID signaling is unavailable. As written, cleanup can become a no-op and leave the sandbox daemon running; callingchild.kill(signal)in the catch path avoids that.services/runner/src/engines/sandbox_agent.ts (2)
946-954: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Only mark the sandbox resumable after storing its reconnect ID.
Line 954 becomes true even without credentials or when the fire-and-forget write fails. Teardown then parks a sandbox that the next turn cannot locate, causing a cold start and leaving the warm sandbox orphaned until auto-delete. Await a success result from
writeSandboxIdand gateresumableon it.
1767-1772: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Destroy failed runs instead of parking their sandbox.
runTurnreturns{ ok: false }for errors and run-limit trips, so thisfinallystill useskeepWarm: true. The next turn can reconnect the same wedged sandbox. Capture the result and only keep it warm whenresult.okis true.Proposed fix
+ let result: AgentRunResult | undefined; try { - return await runTurn(env, request, emit, signal, { + result = await runTurn(env, request, emit, signal, { loaded: env.loadedFromContinuity, }); + return result; } finally { - await env.destroy({ keepWarm: env.resumable && !signal?.aborted }); + await env.destroy({ + keepWarm: Boolean(result?.ok && env.resumable && !signal?.aborted), + }); }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.let result: AgentRunResult | undefined; try { result = await runTurn(env, request, emit, signal, { loaded: env.loadedFromContinuity, }); return result; } finally { // Park a remote session-owned sandbox to warm on a clean turn-end; an aborted run destroys. await env.destroy({ keepWarm: Boolean(result?.ok && env.resumable && !signal?.aborted), });services/runner/src/engines/sandbox_agent/sandbox-reconnect.ts (1)
33-36: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash rg -n -C3 'AbortSignal\.timeout|new AbortController|fetchWith.*Timeout' services/runner/srcRepository: Agenta-AI/agenta
Length of output: 2560
🏁 Script executed:
#!/bin/bash set -euo pipefail printf '\n## outline sandbox-reconnect.ts\n' ast-grep outline services/runner/src/engines/sandbox_agent/sandbox-reconnect.ts --view expanded || true printf '\n## relevant file contents\n' nl -ba services/runner/src/engines/sandbox_agent/sandbox-reconnect.ts | sed -n '1,220p' printf '\n## doFetch definitions/usages\n' rg -n -C3 '\bdoFetch\b|fetch\(' services/runner/src/engines services/runner/src/tools services/runner/src/server.tsRepository: Agenta-AI/agenta
Length of output: 604
🏁 Script executed:
#!/bin/bash set -euo pipefail rg -n -C4 'function doFetch|const doFetch|async function doFetch|export .*doFetch|doFetch\(' services/runner/src | sed -n '1,220p'Repository: Agenta-AI/agenta
Length of output: 8579
🏁 Script executed:
#!/bin/bash set -euo pipefail printf '\n## sandbox-reconnect.ts\n' sed -n '1,220p' services/runner/src/engines/sandbox_agent/sandbox-reconnect.ts printf '\n## session-continuity-durable.ts relevant section\n' sed -n '1,220p' services/runner/src/engines/sandbox_agent/session-continuity-durable.ts printf '\n## fetchImpl usage in sandbox_agent\n' rg -n -C2 'fetchImpl\??:|fetchImpl\s*=' services/runner/src/engines/sandbox_agentRepository: Agenta-AI/agenta
Length of output: 11975
🏁 Script executed:
#!/bin/bash set -euo pipefail printf '\n## sandbox-reconnect.ts with line numbers\n' sed -n '1,140p' services/runner/src/engines/sandbox_agent/sandbox-reconnect.ts | cat -n printf '\n## AbortSignal/fetch timeout usage in sandbox_agent files\n' rg -n -C2 'AbortSignal\.timeout|AbortController|signal:' services/runner/src/engines/sandbox_agentRepository: Agenta-AI/agenta
Length of output: 4025
Add a timeout to both reconnect requests. The GET and PUT both use bare
fetch, so a stalled Daytona API can hang the reconnect path indefinitely. Reuse the runner’s existing abort/timeout pattern here.services/runner/src/engines/sandbox_agent/session-continuity-durable.ts (2)
124-131: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Abort synchronization when the state GET fails.
A transient 401/503 currently becomes
{}, followed by a whole-object PUT that can erase every previously persisted harness record. Missing rows are already represented by a successful response withsession_state: null; non-2xx responses should not write.Proposed fix
const getRes = await doFetch( `${base}/sessions/states/?session_id=${encodeURIComponent(sessionId)}`, { method: "GET", headers: { authorization: deps.authorization } }, ); - const existingData: SessionStateDataWire = getRes.ok - ? (((await getRes.json()) as { session_state?: SessionStateWire | null }) - .session_state?.data ?? {}) - : {}; + if (!getRes.ok) { + log(`sync GET HTTP ${getRes.status} session=${sessionId} — skipping write`); + return; + } + const existingData: SessionStateDataWire = + ((await getRes.json()) as { session_state?: SessionStateWire | null }) + .session_state?.data ?? {};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.const getRes = await doFetch( `${base}/sessions/states/?session_id=${encodeURIComponent(sessionId)}`, { method: "GET", headers: { authorization: deps.authorization } }, ); if (!getRes.ok) { log(`sync GET HTTP ${getRes.status} session=${sessionId} — skipping write`); return; } const existingData: SessionStateDataWire = ((await getRes.json()) as { session_state?: SessionStateWire | null }) .session_state?.data ?? {};
142-163: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Keep the latest session ID aligned with the latest turn.
When the durable row is already at turn 5 and a delayed turn 2 sync arrives, the code keeps
latest_turn_index: 5but replaceslatest_agent_session_idwith turn 2’s ID. Preserve both existing latest fields unless this write advances the counter.Proposed fix
- const latestTurnIndex = Math.max( - existingData.latest_turn_index ?? -1, - turnIndex, - ); + const previousLatestTurnIndex = existingData.latest_turn_index ?? -1; + const latestTurnIndex = Math.max(previousLatestTurnIndex, turnIndex); + const latestAgentSessionId = + turnIndex >= previousLatestTurnIndex + ? agentSessionId + : existingData.latest_agent_session_id; const res = await doFetch( @@ - latest_agent_session_id: agentSessionId, + latest_agent_session_id: latestAgentSessionId,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.// Never regress the cross-harness counter: another harness may already have persisted a // higher latest turn. Take the max so the durable `latest_turn_index` is monotonic. const previousLatestTurnIndex = existingData.latest_turn_index ?? -1; const latestTurnIndex = Math.max(previousLatestTurnIndex, turnIndex); const latestAgentSessionId = turnIndex >= previousLatestTurnIndex ? agentSessionId : existingData.latest_agent_session_id; const res = await doFetch( `${base}/sessions/states/?session_id=${encodeURIComponent(sessionId)}`, { method: "PUT", headers: { "content-type": "application/json", authorization: deps.authorization, }, body: JSON.stringify({ data: { ...existingData, latest_agent_session_id: latestAgentSessionId, latest_turn_index: latestTurnIndex, harness_sessions: merged, },services/runner/src/engines/sandbox_agent/session-continuity.ts (1)
177-197: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Fail closed when the ownership claim is unavailable.
Line 188 states that
undefinedrepresents a network/HTTP claim failure, but Lines 181 and 195 allow the run. During a coordination outage, multiple replicas can therefore cold-start the same local session—the exact split-brain this guard is intended to prevent. A successful first claim should return this replica’s ID; unavailable ownership should raise a distinct coordination error.services/runner/src/sessions/alive.ts (1)
88-126: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash set -euo pipefail # Inspect the target file around the cited lines sed -n '1,220p' services/runner/src/sessions/alive.ts | cat -n printf '\n--- references ---\n' # Find assertLocalRunnerOwnership and ownerReplicaId usages rg -n "assertLocalRunnerOwnership|ownerReplicaId|claimSessionOwnership" services/runner/src -SRepository: Agenta-AI/agenta
Length of output: 9842
🏁 Script executed:
#!/bin/bash set -euo pipefail # Inspect the ownership guard and its call site sed -n '560,700p' services/runner/src/engines/sandbox_agent.ts | cat -n printf '\n--- ownership helpers ---\n' sed -n '300,370p' services/runner/src/engines/sandbox_agent.ts | cat -nRepository: Agenta-AI/agenta
Length of output: 11154
🏁 Script executed:
#!/bin/bash set -euo pipefail # Find and inspect assertLocalRunnerOwnership implementation rg -n "function assertLocalRunnerOwnership|const assertLocalRunnerOwnership|assertLocalRunnerOwnership\(" services/runner/src/engines/sandbox_agent.ts -n -S printf '\n--- definition slice ---\n' sed -n '370,460p' services/runner/src/engines/sandbox_agent.ts | cat -nRepository: Agenta-AI/agenta
Length of output: 4873
🏁 Script executed:
#!/bin/bash set -euo pipefail # Locate the ownership assertion and related logic across the repo rg -n "assertLocalRunnerOwnership" services/runner/src -S printf '\n--- broader owner/claim references ---\n' rg -n "ownerReplicaId|replicaId.*owner|unknown owner|wrong-host|cold-start" services/runner/src -SRepository: Agenta-AI/agenta
Length of output: 2121
🏁 Script executed:
#!/bin/bash set -euo pipefail # Search the entire repository for the assertion definition and usage rg -n "assertLocalRunnerOwnership" . printf '\n--- claimSessionOwnership call sites ---\n' rg -n "claimSessionOwnership\(" .Repository: Agenta-AI/agenta
Length of output: 2218
🏁 Script executed:
#!/bin/bash set -euo pipefail sed -n '180,240p' services/runner/tests/unit/session-continuity.test.ts | cat -n printf '\n--- claim tests ---\n' sed -n '1,160p' services/runner/tests/unit/session-ownership.test.ts | cat -n printf '\n--- ownership test tail ---\n' sed -n '160,260p' services/runner/tests/unit/session-ownership.test.ts | cat -nRepository: Agenta-AI/agenta
Length of output: 12509
Fail closed when the owner claim is unavailable
services/runner/src/sessions/alive.ts:97-124
HTTP/network errors returnownerReplicaId: undefined, andassertLocalRunnerOwnershiptreats that as eligible. During an API outage, multiple local replicas can cold-start the same session and mutate it concurrently. Return a claim failure or retry with a bounded timeout instead.services/runner/tests/unit/session-ownership.test.ts (1)
215-228: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Prevent this unit test from starting a real Daytona sandbox.
With no
startSandboxAgentoverride, the test proceeds into production sandbox startup after proving the resolver was skipped. This can hang, require credentials, or provision external infrastructure. Inject the same fail-fast startup seam used above.
mmabrouk
left a comment
There was a problem hiding this comment.
This is a review-readability pass on this PR: I rewrote the description (context, lifecycle, decisions, risks, reading order, QA plan) and left the inline notes below at the decision points. The notes explain what each piece does, why, and what to scrutinize. They are guidance for reviewers, not change requests.
One finding that cannot be an inline comment because GitHub will not render the file: services/runner/src/engines/sandbox_agent/session-continuity.ts contains literal NUL bytes (the map-key separator in key() and the prefix check in clear() are raw 0x00 characters), so git flags the file as binary. The PR's most central new file shows as Bin 0 -> 8125 bytes: no diff, no inline comments, and every future change to it will be equally un-reviewable. The runtime behavior is fine; please replace the raw byte with the escape sequence "\u0000" (or a plain separator like "\n") so the file becomes reviewable text. Until then, read it locally; the PR description's "How to review" section has a suggested reading order.
- compose: forward DAYTONA_AUTOSTOP/AUTOARCHIVE/AUTODELETE to the runner service in all 7 compose files. They were documented in every env example but never wired, so setting them was a no-op (helm already wired them). - sandbox_agent: export sendLastMessageOnly() and call it from runTurn, so continuation.test.ts pins the shipped decision instead of a local copy. - session_states acceptance: drop the "NOT RUN" note; 14 pass against a live stack built from this branch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…anch QA captures + allow-mode bash scenarios
Context
Until now, every agent turn started from scratch. The runner created a fresh sandbox, called
session/new, and replayed the entire conversation as one flattened text prompt (the "cold replay"). Long sessions got slower and more expensive with every turn, and the harness lost its native session memory each time.This PR gives sessions continuity. The runner now remembers which sandbox a session ran in and which native harness session it produced. On the next turn it reconnects to that sandbox and asks the harness to load its own saved session (ACP
session/load). When that works, the runner sends only the new user message instead of the whole transcript. Every rung of this ladder degrades gracefully; the worst case is exactly today's behavior (fresh sandbox, cold replay).The lifecycle
A session's sandbox moves through five states, driven by three Daytona idle timers plus an explicit "park" at the end of each clean turn:
DAYTONA_AUTOSTOP)session/load, send only the new messageDAYTONA_AUTOARCHIVE)DAYTONA_AUTODELETE), or destroy on abortsession/loadresume; else cold replaysession/newBefore: Daytona sandboxes were created
ephemeral: truewith a 15-minute auto-stop that deleted them. Every turn was effectively "new".Changes
Runner: continuity state (
services/runner/src/engines/sandbox_agent/)session-continuity.ts(new): in-memory store mapping(sessionId, harness)to{agentSessionId, turnIndex}, plus a per-sessionlatestTurnIndex. The core policy is the staleness guard: a harness maysession/loadonly if it authored the most recent completed turn. This is what makes switching between Pi and Claude inside one session safe; the harness that got switched away from replays cold once, then resumes natively.session-continuity-durable.ts(new): mirrors that store into the API'ssession_states.datarow so a runner restart does not lose continuity. It hydrates on session setup and syncs after each completed turn (fire and forget, GET then PUT).sandbox-reconnect.ts(new): reads and writessession_states.sandbox_id. This field changes meaning. Before it held a display label (the provider name, e.g. "local"). Now it holds the actual Daytona instance id used to reconnect.Runner: engine integration (
sandbox_agent.ts)session/loadinstead ofsession/newwhen an eligible native session exists; only then does the turn send just the new user text (RunTurnOptions.loaded).destroy({keepWarm}): only clean successful turns park warm; failed turns and aborts destroy the sandbox.keepWarmis gated onshouldPark()which checks the turn result, client connection, and abort signal.~/.claude/projectsand Pi's sessions dir get the same durable geesefs mount treatment as the session cwd, so even a dead sandbox can resume natively. The Claude mount deliberately covers only the transcript dir, never~/.claude/.credentials.json.-fflag) with a liveness poll that verifies the mount serves I/O before returning.Runner: provider and vendored patch
provider.ts:ephemeral: falseplus the stop/archive/delete timers (defaults 5/15/30 minutes, overridable viaDAYTONA_AUTOSTOP/DAYTONA_AUTOARCHIVE/DAYTONA_AUTODELETE).patches/sandbox-agent@0.4.2.patch(new): teaches the vendored sandbox-agent to try ACPsession/load(with Claude's resume-by-id option in_meta) before its existing text-replay +session/newpath, and kills the daemon's whole process group so ACP subprocesses stop leaking to PID 1.API (
api/oss/src/)Small and surgical: no new tables, no migrations, no new endpoints.
session_states.datagoes from an opaque dict to a typed contract. Before:{"anything": ...}. After:{"latest_agent_session_id": "...", "latest_turn_index": 3, "harness_sessions": {"claude": {"agent_session_id": "...", "turn_index": 3}}}replica_idfield reporting who actually holds the session. The runner compares it to its own id and refuses to serve if it lost.GET /sessions/mounts/signgains anamequery param (default"cwd") so the runner signs the per-harness transcript mounts through the sameRUN_SESSIONSpermission check.Infra, docs, CI
Dockerfile.gh; both images now copypatches/so pnpm can apply the sandbox-agent patch. CI workflow and the Railway build script point at the new path.DAYTONA_AUTOSTOP/DAYTONA_AUTOARCHIVE/DAYTONA_AUTODELETEto the runner container (were documented but not wired). Ngrok tunnel moved fromremoteprofile towith-tunnel(shared with composio); it exits 0 on missingNGROK_AUTHTOKENand binds the agent API on all interfaces so the runner can discover the tunnel URL.02-configuration.mdx, guide 09 gains a "Sandbox lifecycle" section).AGENTA_SESSION_HARNESS_MOUNTS=falsedisables the transcript mounts.Generated clients (Fern, Python and TS)
The sessions delta above, plus a catch-up sweep: the tools-surface changes (new
list_tool_categories, thecategoryfilter, theneeds_approvalfield removal) come from API changes that were already on big-agents with stale generated clients. They are correct against the current API but are not part of this feature.Tests
About 1,300 lines of new runner unit tests (continuity store, durable mirror, reconnect, lifecycle policy, ownership) and new API tests (owner claim, DTO mapping, a live-stack acceptance suite). Two drive-bys ride along: a hermeticity fix so ambient
AGENTA_INSECURE_EGRESS_ALLOWED/DAYTONA_*shell vars cannot flip SSRF test assertions (new conftest pins plus anallow_insecure_envmarker), and fixes for sdk/api tests that were already red on big-agents.Decisions and tradeoffs
max()on the shared turn counter.dist/code and embeds a Claude-specific_metaresume contract, so it is fragile across upstream bumps.Scope / risk
session-continuity.tsis flagged binary by git. The file contains literal NUL bytes (used as a map-key separator), so GitHub renders no diff and accepts no inline comments on the PR's most central new file. Replace the raw bytes with the"\^@"escape (identical behavior) so the file is reviewable, here and in every future PR that touches it.dataobject, andwriteSandboxIdPUTs against the same row. Two concurrent writers can drop a harness entry. The failure mode is a silent cold replay, not corruption, but the PUT semantics (field-patch vs row-replace) need confirming on the API side.sandbox_idvalues hold provider names ("local"), so a migrated session attempts one doomed reconnect, logs, and self-heals by overwriting. Old opaquedatablobs validate away silently; a type collision makes the row read as "no state" via a suppressed exception.clear_ownernow has zero callers. Kill a session on replica A and restart it on replica B within 120 s, and B refuses to serve until the TTL expires. The old steal-happy code did not have this window.replica_idin the heartbeat response andnameonmounts/sign; an old API silently signs the wrong mount.Tests / CI status
session/loadrung (eligible id found, load attempted, fallback on failure) has no test; (2) the acceptance suite: 14 tests pass against a live stack built from this branch.run-web-unit-testsfails with 22 failures in@agenta/entities, but the identical 22 fail on the big-agents base, so they predate this branch. This PR actually fixes the API and SDK unit jobs that are red on base.Commands:
How to review
Suggested reading order (inline comments mark the decision points):
session-continuity.tsrenders as binary on GitHub (see Scope / risk), and it is the file everything else orbits. The policy functions (staleness guard, ownership) are pure and short.session-continuity-durable.tsandsandbox-reconnect.ts: the persistence contract againstsession_states.sandbox_agent.ts: the integration. The interesting spots are the ownership guard (~line 591), the reconnect ladder (~930), thesession/loadbridge (~1093-1158), the post-turn record (~1687), andkeepWarmgated onshouldPark()(~1884).provider.tsandmount.ts: the lifecycle timers and the transcript mounts.mount.tsincludes the store reachability logic that avoids tunneling public S3 endpoints.dbs/redis/sessions/{contract,locks}.pyandcore/sessions/streams/service.pyfor the claim;core/sessions/states/dtos.pyfor the data contract.patches/sandbox-agent@0.4.2.patch: two hunks,session/loadsupport and the process-group kill.How to QA
Prerequisites: EE dev stack with Daytona credentials (
SANDBOX_AGENT_DAYTONA_*), runner image rebuilt from this branch (the pnpm patch changes the install, so a plain restart is not enough).Steps:
session/load; the agent remembers turn 1 natively and the prompt contains only the new message.Edge cases to watch: kill a session and restart it immediately (the 120 s owner TTL window); two local runners against one session (must fail loudly, not cold-start).