feat(sessions): sessions integration — storage rework, reliable approvals, and approval UX#5436
feat(sessions): sessions integration — storage rework, reliable approvals, and approval UX#5436mmabrouk wants to merge 31 commits into
Conversation
Rebases the backend two-thirds of JP's feat/sessions-extensions (PR #5363) onto current main (v0.105.5). Covers api, SDK, Fern python client, and the web generated client: the append-only session_turns domain replacing session_states, the stream command discriminator rename (prompt -> inputs/data), the root /sessions router and SessionsService, /kill teardown wiring, span identity columns plus ag.agent.id, and mounts agent_id. Ported from feat/sessions-extensions@1532ec7fe5 (JP). Reconciliations with the release main: - Kept main's pi-openai model_ref threading (PiHarness/AgentaHarness) and the EndpointResolutionError export alongside JP's HarnessType -> HarnessKind rename. - Kept main's 0.105.5 version in api/pyproject.toml while re-adding JP's greenlet dependency; api/uv.lock updated for greenlet only. - Kept main's sdks/python and services uv.lock (JP's were stale transitive bumps). Claude-Session: https://claude.ai/code/session_01DnWRxU3dCJ11hgDidm26vW
The record-ingest span_id is an OpenTelemetry span id (64-bit / 16 hex chars), not a UUID (128-bit / 32 hex). Typed as Optional[UUID], pydantic rejected every runner ingest with a 422 and session records silently dropped. Model it as what it is end to end: a validated 16-hex string (OTelSpanId) on the API model, core DTOs, and DAO column; varchar in the two unreleased migrations. trace_id stays UUID (genuinely the 32-hex OTel trace id). Adds contract + backfill + multi-turn tests. Claude-Session: https://claude.ai/code/session_01DnWRxU3dCJ11hgDidm26vW
Rebases the runner third of JP's feat/sessions-extensions (PR #5363) onto current main, ported into the post-decomposition module layout (PR #5369). JP wrote these against the old monolithic sandbox_agent.ts; they are re-homed by region into the decomposed modules: - The SandboxAgentDeps interface change (syncHarnessSessionDurable renamed to appendSessionTurn; the write/clear sandbox-pointer deps dropped) goes into runtime-contracts.ts. - The two acquireEnvironment branches (the reconnect-failure path and the post-hydrate pointer write, both now append-only with no pre-turn pointer PUT) go into environment.ts. - The turn-completion sync (syncHarnessSessionDurable replaced by appendSessionTurn with the streamId gate, workflow references, sandbox id, and trace id) goes into run-turn.ts. The sandbox-reconnect.ts and session-continuity-durable.ts rewrites apply as-is. protocol.ts, sessions/alive.ts, sessions/persist.ts, version.ts, and the tests are JP's versions verbatim. Ported from feat/sessions-extensions@1532ec7fe5 (JP). Reconciliations with the release main: - server.ts: kept main's session-identity / session-pool import split from the decomposition and applied JP's watchdog changes (startAliveWatchdog is now awaited and takes an onInterrupted callback that aborts the controller, request.streamId is threaded from the heartbeat, and buildPersistingEmitter gets turnId and span_id). - tracing/otel.ts, package.json, pnpm-lock.yaml: kept main's. The OTel 2.x bump JP carried is already in main, and main additionally has the #5364 trace-id-on-done feature that JP's branch predates. Verified: pnpm run typecheck clean, pnpm test 1202/1202 pass, build:extension ok. Claude-Session: https://claude.ai/code/session_01DnWRxU3dCJ11hgDidm26vW
…persist test Add a persist test whose fetch stub enforces the API contract (span_id must be a 16-hex OTel span id, else 422), so a regression to a non-span-shaped span_id fails here instead of silently dropping records. The old stub returned 200 for any body and hid the mismatch. Also switch the turn/span tagging test to a real 16-hex span id. Claude-Session: https://claude.ai/code/session_01DnWRxU3dCJ11hgDidm26vW
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughChangesThe session platform replaces standalone session state with append-only session turns and stream headers. It adds lifecycle and turn APIs, tracing and mount identity metadata, runner continuity and teardown flows, synchronized clients and SDKs, deployment wiring, and validation coverage. Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Runner
participant SessionAPI
participant SessionsService
participant Postgres
Runner->>SessionAPI: Heartbeat and append or complete turn
SessionAPI->>SessionsService: Process turn or lifecycle request
SessionsService->>Postgres: Query or persist streams and turns
Postgres-->>SessionsService: Session data
SessionsService-->>SessionAPI: API response
SessionAPI-->>Runner: Heartbeat or turn response
Possibly related PRs
🚥 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 |
Railway Preview Environment
|
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
services/runner/src/sessions/alive.ts (1)
60-105: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winAdd a timeout to
sendHeartbeat
startAliveWatchdogawaits the first heartbeat before returning, so a stalled sessions API blocks session-owned turn start for the full fetch timeout. Add anAbortSignaltimeout here so it fails open quickly instead of delaying every run.
🧹 Nitpick comments (3)
sdks/python/oss/tests/pytest/unit/agents/test_dtos_capabilities_events.py (1)
80-87: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRename test function to reflect the new enum.
Since
HarnessTypewas renamed toHarnessKind, it would be good to update the test function name as well to maintain consistency.♻️ Proposed refactor
-def test_harness_type_coerce(): +def test_harness_kind_coerce(): assert HarnessKind.coerce(HarnessKind.PI) is HarnessKind.PIsdks/python/agenta/sdk/agents/adapters/harnesses.py (1)
172-179: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueCatch
ValueErrorfromcoerceto display the friendly error message.
HarnessKind.coerce()will raise aValueErrorif passed an invalid harness string (since it attempts to instantiate the enum). This bypasses theexcept KeyErrorblock, meaning the helpful error message listingknown harnessesis never shown for invalid input.Consider catching both
ValueErrorandKeyErrorto ensure the friendly message is always used.♻️ Proposed refactor
- resolved = HarnessKind.coerce(harness_type) try: + resolved = HarnessKind.coerce(harness_type) cls = _HARNESSES[resolved] - except KeyError as exc: + except (ValueError, KeyError) as exc: known = ", ".join(sorted(h.value for h in _HARNESSES)) + val = harness_type.value if isinstance(harness_type, HarnessKind) else harness_type raise ValueError( - f"unknown harness '{resolved.value}'; known harnesses: {known}" + f"unknown harness '{val}'; known harnesses: {known}" ) from excweb/oss/src/components/SessionInspector/api.ts (1)
40-46: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDeduplicate
fetchStateandfetchStream.With this change, the
fetchStateimplementation is now completely identical tofetchStream(lines 28-34). If the namefetchStatemust be kept for backward compatibility with existing callers, consider aliasing it to avoid duplicating the API call logic.♻️ Proposed refactor
-export async function fetchState(sessionId: string, projectId?: string | null) { - const res = await client().sessions.fetchSessionStream( - {session_id: sessionId}, - scope(projectId), - ) - return res.stream ?? null -} +export const fetchState = fetchStream
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 0b626118-8487-4533-b642-2e52b3c47908
⛔ Files ignored due to path filters (51)
api/uv.lockis excluded by!**/*.lockweb/packages/agenta-api-client/src/generated/api/resources/mounts/client/Client.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/mounts/client/requests/MountQueryRequest.tsis excluded by!**/generated/**web/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/ArchiveSessionRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/DeleteSessionRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/FetchTurnRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SessionDetachRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SessionHeartbeatRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SessionQueryRequest.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/SessionStreamCommandRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SessionStreamQueryRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SessionTurnAppendRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SessionTurnQueryRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SetSessionStreamHeaderRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/UnarchiveSessionRequest.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/index.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/CommandMode.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/HarnessKind.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/Mount.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/MountCreate.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/MountQuery.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/SessionHeartbeatResult.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionInteraction.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionMount.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionMountQuery.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionRecord.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionResponse.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/SessionStateFlags.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/SessionStream.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionStreamCommandResponse.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionStreamHeaderEdit.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionStreamResponse.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionStreamsResponse.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionTurn.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionTurnQuery.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionTurnResponse.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionTurnsResponse.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SessionsResponse.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SpanInput.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SpanOutput.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SpansNodeInput.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/SpansNodeOutput.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/WorkflowRequestData.tsis excluded by!**/generated/**web/packages/agenta-api-client/src/generated/api/types/index.tsis excluded by!**/generated/**
📒 Files selected for processing (195)
api/entrypoints/routers.pyapi/oss/databases/postgres/migrations/core/env.pyapi/oss/databases/postgres/migrations/core_oss/versions/oss000000014_add_session_turns.pyapi/oss/databases/postgres/migrations/core_oss/versions/oss000000015_add_session_streams_header.pyapi/oss/databases/postgres/migrations/core_oss/versions/oss000000016_add_mounts_agent_id.pyapi/oss/databases/postgres/migrations/core_oss/versions/oss000000017_drop_session_states.pyapi/oss/databases/postgres/migrations/tracing_oss/versions/oss000000003_add_span_identity_columns.pyapi/oss/databases/postgres/migrations/tracing_oss/versions/oss000000004_add_records_turn_span.pyapi/oss/src/apis/fastapi/mounts/router.pyapi/oss/src/apis/fastapi/mounts/utils.pyapi/oss/src/apis/fastapi/otlp/extractors/adapters/logfire_adapter.pyapi/oss/src/apis/fastapi/otlp/extractors/canonical_attributes.pyapi/oss/src/apis/fastapi/otlp/extractors/span_data_builders.pyapi/oss/src/apis/fastapi/otlp/utils/serialization.pyapi/oss/src/apis/fastapi/sessions/models.pyapi/oss/src/apis/fastapi/sessions/router.pyapi/oss/src/apis/fastapi/tracing/router.pyapi/oss/src/core/mounts/dtos.pyapi/oss/src/core/mounts/interfaces.pyapi/oss/src/core/mounts/service.pyapi/oss/src/core/sessions/dtos.pyapi/oss/src/core/sessions/interactions/dtos.pyapi/oss/src/core/sessions/interactions/interfaces.pyapi/oss/src/core/sessions/interactions/service.pyapi/oss/src/core/sessions/records/dtos.pyapi/oss/src/core/sessions/service.pyapi/oss/src/core/sessions/states/dtos.pyapi/oss/src/core/sessions/states/interfaces.pyapi/oss/src/core/sessions/states/service.pyapi/oss/src/core/sessions/streams/dtos.pyapi/oss/src/core/sessions/streams/interfaces.pyapi/oss/src/core/sessions/streams/runner_client.pyapi/oss/src/core/sessions/streams/service.pyapi/oss/src/core/sessions/turns/__init__.pyapi/oss/src/core/sessions/turns/dtos.pyapi/oss/src/core/sessions/turns/interfaces.pyapi/oss/src/core/sessions/turns/service.pyapi/oss/src/core/sessions/turns/types.pyapi/oss/src/core/shared/dtos.pyapi/oss/src/core/tracing/dtos.pyapi/oss/src/core/tracing/service.pyapi/oss/src/core/tracing/utils/filtering.pyapi/oss/src/core/tracing/utils/trees.pyapi/oss/src/dbs/postgres/mounts/dao.pyapi/oss/src/dbs/postgres/mounts/dbas.pyapi/oss/src/dbs/postgres/mounts/dbes.pyapi/oss/src/dbs/postgres/mounts/mappings.pyapi/oss/src/dbs/postgres/sessions/interactions/dao.pyapi/oss/src/dbs/postgres/sessions/records/dao.pyapi/oss/src/dbs/postgres/sessions/records/dbas.pyapi/oss/src/dbs/postgres/sessions/records/dbes.pyapi/oss/src/dbs/postgres/sessions/records/mappings.pyapi/oss/src/dbs/postgres/sessions/states/dao.pyapi/oss/src/dbs/postgres/sessions/states/dbes.pyapi/oss/src/dbs/postgres/sessions/states/mappings.pyapi/oss/src/dbs/postgres/sessions/streams/dao.pyapi/oss/src/dbs/postgres/sessions/streams/dbes.pyapi/oss/src/dbs/postgres/sessions/streams/mappings.pyapi/oss/src/dbs/postgres/sessions/turns/__init__.pyapi/oss/src/dbs/postgres/sessions/turns/dao.pyapi/oss/src/dbs/postgres/sessions/turns/dbas.pyapi/oss/src/dbs/postgres/sessions/turns/dbes.pyapi/oss/src/dbs/postgres/sessions/turns/mappings.pyapi/oss/src/dbs/postgres/sessions/turns/utils.pyapi/oss/src/dbs/postgres/tracing/dao.pyapi/oss/src/dbs/postgres/tracing/dbas.pyapi/oss/src/dbs/postgres/tracing/dbes.pyapi/oss/src/dbs/postgres/tracing/mappings.pyapi/oss/src/dbs/postgres/tracing/utils.pyapi/oss/src/utils/env.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/acceptance/sessions/__init__.pyapi/oss/tests/pytest/acceptance/sessions/test_archive_unarchive.pyapi/oss/tests/pytest/acceptance/sessions/test_records_ingest_contract.pyapi/oss/tests/pytest/acceptance/sessions/test_stream_header_basics.pyapi/oss/tests/pytest/acceptance/sessions/test_stream_header_roundtrip.pyapi/oss/tests/pytest/unit/mounts/test_agent_id_backfill.pyapi/oss/tests/pytest/unit/mounts/test_agent_mounts.pyapi/oss/tests/pytest/unit/otlp/test_logfire_adapter.pyapi/oss/tests/pytest/unit/session_states/__init__.pyapi/oss/tests/pytest/unit/session_states/test_harness_sessions_mapping.pyapi/oss/tests/pytest/unit/session_states/test_session_id_validation.pyapi/oss/tests/pytest/unit/sessions/conftest.pyapi/oss/tests/pytest/unit/sessions/test_command_matrix_inputs_data.pyapi/oss/tests/pytest/unit/sessions/test_heartbeat_is_current_turn.pyapi/oss/tests/pytest/unit/sessions/test_kill_runner_teardown.pyapi/oss/tests/pytest/unit/sessions/test_mounts_agent_id_backfill.pyapi/oss/tests/pytest/unit/sessions/test_record_ingest_endpoint.pyapi/oss/tests/pytest/unit/sessions/test_records_mapping_upsert.pyapi/oss/tests/pytest/unit/sessions/test_records_turn_span_dao.pyapi/oss/tests/pytest/unit/sessions/test_runner_client_kill.pyapi/oss/tests/pytest/unit/sessions/test_sessions_root_service.pyapi/oss/tests/pytest/unit/sessions/test_stream_header_merge.pyapi/oss/tests/pytest/unit/sessions/test_turns_dao.pyapi/oss/tests/pytest/unit/sessions/test_turns_dao_conflict.pyapi/oss/tests/pytest/unit/sessions/test_wp5_dao_fanout.pyapi/oss/tests/pytest/unit/sessions/test_wp5_mount_teardown.pyapi/oss/tests/pytest/unit/sessions/test_wp5_root_router.pyapi/oss/tests/pytest/unit/tracing/test_dao_ingest_identity_columns.pyapi/oss/tests/pytest/unit/tracing/utils/test_trees.pyapi/pyproject.tomlclients/python/agenta_client/__init__.pyclients/python/agenta_client/mounts/client.pyclients/python/agenta_client/mounts/raw_client.pyclients/python/agenta_client/sessions/client.pyclients/python/agenta_client/sessions/raw_client.pyclients/python/agenta_client/types/__init__.pyclients/python/agenta_client/types/harness_kind.pyclients/python/agenta_client/types/harness_session_record.pyclients/python/agenta_client/types/mount.pyclients/python/agenta_client/types/mount_create.pyclients/python/agenta_client/types/mount_query.pyclients/python/agenta_client/types/session_heartbeat_result.pyclients/python/agenta_client/types/session_interaction.pyclients/python/agenta_client/types/session_mount.pyclients/python/agenta_client/types/session_mount_query.pyclients/python/agenta_client/types/session_record.pyclients/python/agenta_client/types/session_response.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/session_stream.pyclients/python/agenta_client/types/session_stream_command_response.pyclients/python/agenta_client/types/session_stream_header_edit.pyclients/python/agenta_client/types/session_stream_response.pyclients/python/agenta_client/types/session_streams_response.pyclients/python/agenta_client/types/session_turn.pyclients/python/agenta_client/types/session_turn_query.pyclients/python/agenta_client/types/session_turn_response.pyclients/python/agenta_client/types/session_turns_response.pyclients/python/agenta_client/types/sessions_response.pyclients/python/agenta_client/types/span_input.pyclients/python/agenta_client/types/span_output.pyclients/python/agenta_client/types/spans_node_input.pyclients/python/agenta_client/types/spans_node_output.pyclients/python/agenta_client/types/workflow_request_data.pydocs/docs/self-host/reference/01-configuration.mdxhosting/docker-compose/ee/docker-compose.dev.ymlhosting/docker-compose/ee/docker-compose.gh.ymlhosting/docker-compose/oss/docker-compose.dev.ymlhosting/docker-compose/oss/docker-compose.gh.ymlhosting/kubernetes/helm/templates/api-deployment.yamlsdks/python/agenta/sdk/agents/__init__.pysdks/python/agenta/sdk/agents/adapters/harnesses.pysdks/python/agenta/sdk/agents/adapters/local.pysdks/python/agenta/sdk/agents/adapters/sandbox_agent.pysdks/python/agenta/sdk/agents/dtos.pysdks/python/agenta/sdk/agents/errors.pysdks/python/agenta/sdk/agents/interfaces.pysdks/python/agenta/sdk/agents/utils/wire.pysdks/python/agenta/sdk/engines/tracing/tracing.pysdks/python/agenta/sdk/middlewares/running/normalizer.pysdks/python/agenta/sdk/models/tracing.pysdks/python/oss/tests/pytest/integration/agents/_fake_runner_backend.pysdks/python/oss/tests/pytest/integration/agents/_qa_transcripts.pysdks/python/oss/tests/pytest/unit/agents/conftest.pysdks/python/oss/tests/pytest/unit/agents/connections/test_dtos_model_ref.pysdks/python/oss/tests/pytest/unit/agents/skills/test_skills_e2e.pysdks/python/oss/tests/pytest/unit/agents/test_agent_composition_seam.pysdks/python/oss/tests/pytest/unit/agents/test_dtos_capabilities_events.pysdks/python/oss/tests/pytest/unit/agents/test_environment_lifecycle.pysdks/python/oss/tests/pytest/unit/agents/test_harness_adapters.pysdks/python/oss/tests/pytest/unit/agents/test_harness_identity.pysdks/python/oss/tests/pytest/unit/agents/test_wire_contract.pysdks/python/oss/tests/pytest/unit/test_batch_fold_stream_contract_routing.pysdks/python/oss/tests/pytest/unit/test_invoke_real_handlers_negotiation_routing.pysdks/python/oss/tests/pytest/unit/test_normalizer_passthrough.pysdks/python/oss/tests/pytest/unit/test_tracing_store_agent.pyservices/oss/tests/pytest/unit/agent/conftest.pyservices/runner/src/engines/sandbox_agent/environment.tsservices/runner/src/engines/sandbox_agent/run-turn.tsservices/runner/src/engines/sandbox_agent/runtime-contracts.tsservices/runner/src/engines/sandbox_agent/sandbox-reconnect.tsservices/runner/src/engines/sandbox_agent/session-continuity-durable.tsservices/runner/src/protocol.tsservices/runner/src/server.tsservices/runner/src/sessions/alive.tsservices/runner/src/sessions/persist.tsservices/runner/src/version.tsservices/runner/tests/acceptance/server-contract.test.tsservices/runner/tests/integration/server-smoke.test.tsservices/runner/tests/unit/sandbox-lifecycle.test.tsservices/runner/tests/unit/sandbox-reconnect.test.tsservices/runner/tests/unit/server.test.tsservices/runner/tests/unit/session-alive-interrupt.test.tsservices/runner/tests/unit/session-alive.test.tsservices/runner/tests/unit/session-continuity-durable.test.tsservices/runner/tests/unit/session-keepalive-engine.test.tsservices/runner/tests/unit/session-persist.test.tsweb/oss/src/components/SessionInspector/api.tsweb/packages/agenta-entities/src/session/api/api.tsweb/packages/agenta-entities/src/session/core/liveness.tsweb/packages/agenta-entities/src/session/core/schema.tsweb/packages/agenta-entities/src/session/index.ts
💤 Files with no reviewable changes (17)
- api/oss/tests/pytest/unit/session_states/test_harness_sessions_mapping.py
- clients/python/agenta_client/types/session_state_data.py
- api/oss/src/dbs/postgres/sessions/states/dbes.py
- api/oss/tests/pytest/acceptance/session_states/test_harness_sessions_roundtrip.py
- clients/python/agenta_client/types/harness_session_record.py
- api/oss/src/core/sessions/states/service.py
- api/oss/src/core/sessions/states/interfaces.py
- clients/python/agenta_client/types/session_state_upsert_request.py
- api/oss/tests/pytest/unit/session_states/test_session_id_validation.py
- api/oss/databases/postgres/migrations/core/env.py
- api/oss/src/core/sessions/states/dtos.py
- api/oss/src/dbs/postgres/sessions/states/mappings.py
- api/oss/src/dbs/postgres/sessions/states/dao.py
- api/oss/tests/pytest/acceptance/session_states/test_session_states_basics.py
- web/packages/agenta-entities/src/session/core/schema.ts
- web/packages/agenta-entities/src/session/index.ts
- clients/python/agenta_client/types/session_state.py
| project_id: UUID = request.state.project_id | ||
| user_id: UUID = request.state.user_id | ||
|
|
||
| if not await check_action_access( | ||
| user_uid=str(user_id), | ||
| project_id=str(project_id), | ||
| permission=Permission.RUN_SESSIONS, | ||
| ): | ||
| raise FORBIDDEN_EXCEPTION | ||
|
|
||
| turn = await self.turns_service.append_turn( | ||
| project_id=project_id, | ||
| user_id=user_id, | ||
| turn=SessionTurnCreate( | ||
| session_id=body.session_id, | ||
| stream_id=body.stream_id, | ||
| turn_index=body.turn_index, | ||
| harness_kind=body.harness_kind, | ||
| agent_session_id=body.agent_session_id, | ||
| sandbox_id=body.sandbox_id, | ||
| references=body.references, | ||
| trace_id=body.trace_id, | ||
| span_id=body.span_id, | ||
| start_time=body.start_time, | ||
| end_time=body.end_time, | ||
| ), | ||
| ) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# What type is request.state.project_id set to? Find the middleware/dependency that sets it.
rg -nP --type=py -C3 '\.state\.project_id\s*='
rg -nP --type=py -C3 '\.state\.user_id\s*='
# Confirm the turns service/DAO signatures expect UUID.
ast-grep run --pattern 'async def append(self, $$$)' --lang python api/oss/src/dbs/postgres/sessions/turns/dao.pyRepository: Agenta-AI/agenta
Length of output: 154
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- locate state assignments ---'
rg -n --type=py 'state\.(project_id|user_id)\s*=' api/oss/src | sed -n '1,200p'
echo '--- locate request.state reads in sessions router ---'
rg -n --type=py 'request\.state\.(project_id|user_id)' api/oss/src/apis/fastapi/sessions/router.py
echo '--- inspect relevant router section ---'
sed -n '1040,1125p' api/oss/src/apis/fastapi/sessions/router.py
echo '--- inspect sibling handlers around query_sessions/delete_session ---'
sed -n '1200,1265p' api/oss/src/apis/fastapi/sessions/router.py
echo '--- inspect turns service signatures/usages ---'
rg -n --type=py 'def (append_turn|query_turns|fetch_turn)\b|class .*Turn|project_id: UUID|user_id: UUID' api/oss/src | sed -n '1,220p'
echo '--- inspect turns dao ---'
fd -a 'turns' api/oss/src/dbs/postgres | sed -n '1,20p'
if [ -f api/oss/src/dbs/postgres/sessions/turns/dao.py ]; then
sed -n '1,220p' api/oss/src/dbs/postgres/sessions/turns/dao.py
fiRepository: Agenta-AI/agenta
Length of output: 31332
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- auth middleware around state assignment sites ---'
sed -n '500,560p' api/oss/src/middlewares/auth.py
printf '\n'
sed -n '740,780p' api/oss/src/middlewares/auth.py
printf '\n'
sed -n '800,830p' api/oss/src/middlewares/auth.py
printf '\n'
sed -n '860,910p' api/oss/src/middlewares/auth.py
echo '--- auth/context types ---'
sed -n '1,120p' api/oss/src/utils/context.py
echo '--- turns service interface and implementation ---'
fd -a 'turns' api/oss/src/core | sed -n '1,20p'
if [ -f api/oss/src/core/sessions/turns/interfaces.py ]; then
sed -n '1,220p' api/oss/src/core/sessions/turns/interfaces.py
fi
if [ -f api/oss/src/core/sessions/turns/service.py ]; then
sed -n '1,260p' api/oss/src/core/sessions/turns/service.py
fi
echo '--- exact router lines around turns handlers ---'
sed -n '1068,1145p' api/oss/src/apis/fastapi/sessions/router.pyRepository: Agenta-AI/agenta
Length of output: 17407
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- identify auth middleware functions that populate request.state ---'
rg -n --type=py 'def verify_(bearer_token|apikey_token|secret_token)|request\.state\.(project_id|user_id)\s*=' api/oss/src/middlewares/auth.py
echo '--- inspect full verify_secret_token and surrounding helper logic ---'
sed -n '840,930p' api/oss/src/middlewares/auth.py
echo '--- inspect full verify_bearer_token and verify_apikey_token headers/state construction ---'
sed -n '500,620p' api/oss/src/middlewares/auth.py
printf '\n'
sed -n '700,840p' api/oss/src/middlewares/auth.py
echo '--- inspect any typed auth context setters/getters used by middleware ---'
rg -n --type=py 'set_auth_context|get_auth_context|AuthScope|AuthContext' api/oss/src/middlewares api/oss/src/utils | sed -n '1,220p'Repository: Agenta-AI/agenta
Length of output: 15503
Normalize request.state.project_id/user_id before calling turns_service. auth.py stores these on request.state as strings, while sibling session handlers coerce with UUID(str(...)) and the turns DAO expects UUIDs. Passing the raw state through here can send strings into the DAO on this path.
| project_id=project_id, | ||
| session_id=request.session_id, | ||
| ) | ||
| return SessionHeartbeatResult(stream=stream, replica_id=owner) | ||
| return SessionHeartbeatResult( | ||
| stream=stream, replica_id=owner, is_current_turn=False | ||
| ) | ||
|
|
||
| # True only when this turn_id still (or again, uninterrupted) owns the alive lock at | ||
| # the moment of this heartbeat. A cancel/steer/kill deletes the alive key entirely, | ||
| # which the nx=True re-acquire below would otherwise silently re-establish under the | ||
| # SAME turn_id, masking the interruption from the runner's watchdog (W7.4 — this is | ||
| # what `is_current_turn` exists to surface). An absent key is ambiguous by itself: it | ||
| # is also the normal state before this turn's VERY FIRST heartbeat (the API's | ||
| # `_start_turn` acquire may not have landed yet, or this beat wins a race with it), and | ||
| # that is NOT an interruption. Disambiguate with the durable row's `turn_id`: if it | ||
| # already recorded THIS turn_id as established (a prior heartbeat's write), the key | ||
| # being gone now is something else's doing; if the row shows no turn yet, or a | ||
| # different one, this is establishment. | ||
| prior_stream = await self._dao.get_by_session_id( | ||
| project_id=project_id, | ||
| session_id=request.session_id, | ||
| ) | ||
| turn_was_established = bool( | ||
| prior_stream and prior_stream.turn_id == request.turn_id | ||
| ) | ||
| is_current_turn = True |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm whether the durable row was ever updated with fresh flags on
# a repeat heartbeat prior to this diff, and check existing test coverage.
git log -p -3 -- api/oss/src/core/sessions/streams/service.py | sed -n '/async def heartbeat/,/async def fetch/p' | head -200
rg -n "is_current_turn|flags" api/oss/tests/pytest/unit/sessions/test_heartbeat_is_current_turn.pyRepository: Agenta-AI/agenta
Length of output: 1035
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## service heartbeat slice\n'
sed -n '300,390p' api/oss/src/core/sessions/streams/service.py
printf '\n## dbes docstring slice\n'
sed -n '1,120p' api/oss/src/dbs/postgres/sessions/streams/dbes.py
printf '\n## heartbeat tests slice\n'
sed -n '1,220p' api/oss/tests/pytest/unit/sessions/test_heartbeat_is_current_turn.pyRepository: Agenta-AI/agenta
Length of output: 12090
Fresh heartbeat flags need to be applied to the existing stream row. Reusing prior_stream bypasses the create/update paths once the row exists, so the newly computed Redis liveness snapshot never gets written back and the returned stream.flags can stay stale. If the write is intentionally deferred, at least copy flags onto stream before returning so the API response reflects the current beat.
| name: typing.Optional[str] = None | ||
| description: typing.Optional[str] = None | ||
| id: typing.Optional[str] = None |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n -A3 'id:' api/oss/src/core/sessions/streams/dtos.py
rg -n -A20 'map_stream_dbe_to_dto|def map_.*stream' api/oss/src/dbs/postgres/sessions/streams/mappings.pyRepository: Agenta-AI/agenta
Length of output: 4091
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== client type =="
cat -n clients/python/agenta_client/types/session_stream.py | sed -n '1,120p'
echo
echo "== DTO / mapping context =="
cat -n api/oss/src/core/sessions/streams/dtos.py | sed -n '1,120p'
echo
cat -n api/oss/src/dbs/postgres/sessions/streams/mappings.py | sed -n '1,120p'Repository: Agenta-AI/agenta
Length of output: 9700
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== shared base DTOs =="
cat -n api/oss/src/core/shared/dtos.py | sed -n '1,220p'
echo
echo "== SessionStream construction sites =="
rg -n "SessionStream\(" api/oss/src clients/python/agenta_client -g '!**/*.pyc' -A3 -B3Repository: Agenta-AI/agenta
Length of output: 4156
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Identifier / Header / Lifecycle definitions =="
rg -n "class Identifier|class Header|class Lifecycle" api/oss/src clients/python/agenta_client -g '*.py' -A6 -B2
echo
echo "== any explicit SessionStream schema/docs =="
rg -n "SessionStream" api/oss/src -g '*.py' -A4 -B2 | sed -n '1,220p'Repository: Agenta-AI/agenta
Length of output: 18885
Keep SessionStream.id required
SessionStream still includes Identifier, and the DBE→DTO mapping always sets id=stream_dbe.id. If there isn’t a real transient stream shape without an ID, the SDK field should stay required; making it optional only weakens the contract for callers.
| if agent_id: | ||
| span.set_attribute("id", agent_id, namespace="agent") | ||
|
|
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Handle raw OpenTelemetry spans by adding a TypeError fallback.
If a raw OpenTelemetry span (such as a NonRecordingSpan during certain internal operations) is retrieved, calling set_attribute with a namespace keyword argument will raise a TypeError. Because this is wrapped in with suppress():, the attribute will be silently dropped.
Please mirror the fallback logic used in store_session to ensure the attribute is recorded successfully on all span types. (Consider applying this to store_user as well, though it's outside this diff.)
🛠️ Proposed fix
if agent_id:
- span.set_attribute("id", agent_id, namespace="agent")
+ try:
+ span.set_attribute("id", agent_id, namespace="agent")
+ except TypeError:
+ span.set_attribute("agent.id", agent_id)📝 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.
| if agent_id: | |
| span.set_attribute("id", agent_id, namespace="agent") | |
| if agent_id: | |
| try: | |
| span.set_attribute("id", agent_id, namespace="agent") | |
| except TypeError: | |
| span.set_attribute("agent.id", agent_id) |
|
Two approved amendments landed: the ledger row is now written at turn start (carrying trace id, span id, and start time, so crashed turns keep their trace link) and completed at turn end through a new guarded idempotent complete operation, with the native-continuation trust rule moved from row-exists to end-time-set; and the turns listing now orders by turn index with the row id as tiebreaker. Runner suite 1241 green, sessions API suite 154 green against real Postgres. |
1367100 to
fab413d
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
services/runner/src/engines/sandbox_agent/run-turn.ts (1)
613-613: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReuse the existing
continuityStorevariable.You can reuse the
continuityStorevariable already declared at the top of the function (line 92) instead of re-evaluating the nullish coalescing expression here.♻️ Proposed refactor
- (deps.sessionContinuityStore ?? sessionContinuityStore).record( + continuityStore.record(api/oss/tests/pytest/unit/sessions/test_turns_dao.py (1)
484-497: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTest comment claims out-of-order coverage that isn't exercised.
The comment says a "late-arriving lower-index write for a stale turn must never win," but all four turns are appended strictly in increasing
turn_indexorder (0→1→2→3), matching insertion order. This doesn't actually validate thatlatest_turn/latest_turn_per_harness_kindrely onORDER BY turn_index DESCrather than insertion order — unliketest_query_turns_orders_by_turn_index_with_id_tiebreaker(Lines 371-382), which deliberately appends indices out of sequence (2, 0, 1) to prove this. Consider appending here in a scrambled order (e.g., turn_index 3 before turn_index 2) to actually validate the claim.♻️ Example: exercise true out-of-order insertion
- latest_pi_core = await dao.append( - project_id=project_id, - user_id=None, - turn=SessionTurnCreate( - session_id=session_id, - stream_id=stream_id, - turn_index=2, - harness_kind=HarnessKind.PI, - agent_session_id="pi-core-sess-2", - sandbox_id="sandbox-2", - ), - ) - - # A late-arriving lower-index write for a stale turn must never win — the - # resume pointer is ORDER BY turn_index DESC, not insertion order. latest_overall = await dao.append( project_id=project_id, user_id=None, turn=SessionTurnCreate( session_id=session_id, stream_id=stream_id, turn_index=3, harness_kind=HarnessKind.CLAUDE, agent_session_id="claude-sess-3", sandbox_id="sandbox-3", ), ) + + # A late-arriving lower-index write for a stale turn must never win — the + # resume pointer is ORDER BY turn_index DESC, not insertion order. + latest_pi_core = await dao.append( + project_id=project_id, + user_id=None, + turn=SessionTurnCreate( + session_id=session_id, + stream_id=stream_id, + turn_index=2, + harness_kind=HarnessKind.PI, + agent_session_id="pi-core-sess-2", + sandbox_id="sandbox-2", + ), + )
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 2f0d6fac-34f3-4179-b610-a41d318b3d9c
📒 Files selected for processing (13)
api/oss/src/apis/fastapi/sessions/models.pyapi/oss/src/apis/fastapi/sessions/router.pyapi/oss/src/core/sessions/turns/dtos.pyapi/oss/src/core/sessions/turns/interfaces.pyapi/oss/src/core/sessions/turns/service.pyapi/oss/src/core/sessions/turns/types.pyapi/oss/src/dbs/postgres/sessions/turns/dao.pyapi/oss/tests/pytest/unit/sessions/test_turns_complete.pyapi/oss/tests/pytest/unit/sessions/test_turns_dao.pyservices/runner/src/engines/sandbox_agent/run-turn.tsservices/runner/src/engines/sandbox_agent/session-continuity-durable.tsservices/runner/tests/unit/session-continuity-durable.test.tsservices/runner/tests/unit/session-keepalive-engine.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
- api/oss/src/core/sessions/turns/interfaces.py
- api/oss/src/core/sessions/turns/dtos.py
- services/runner/tests/unit/session-keepalive-engine.test.ts
- api/oss/src/apis/fastapi/sessions/models.py
- services/runner/src/engines/sandbox_agent/session-continuity-durable.ts
- services/runner/tests/unit/session-continuity-durable.test.ts
| @intercept_exceptions() | ||
| async def complete_turn( | ||
| self, | ||
| request: Request, | ||
| body: SessionTurnCompleteRequest, | ||
| ) -> SessionTurnResponse: | ||
| project_id: UUID = request.state.project_id | ||
| user_id: UUID = request.state.user_id | ||
|
|
||
| if not await check_action_access( | ||
| user_uid=str(user_id), | ||
| project_id=str(project_id), | ||
| permission=Permission.RUN_SESSIONS, | ||
| ): | ||
| raise FORBIDDEN_EXCEPTION | ||
|
|
||
| try: | ||
| turn = await self.turns_service.complete_turn( | ||
| project_id=project_id, | ||
| turn=SessionTurnComplete( | ||
| session_id=body.session_id, | ||
| turn_index=body.turn_index, | ||
| agent_session_id=body.agent_session_id, | ||
| end_time=body.end_time, | ||
| ), | ||
| ) | ||
| except SessionTurnNotFound as e: | ||
| raise HTTPException( | ||
| status_code=status.HTTP_404_NOT_FOUND, | ||
| detail=e.message, | ||
| ) from e | ||
| return SessionTurnResponse(count=1, turn=turn) | ||
|
|
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Coerce request.state.project_id/user_id to UUID before calling the service.
complete_turn passes request.state.project_id/user_id straight through as a type-annotated (but not actually coerced) UUID. Elsewhere in this same file, set_session_stream_header (UUID(request.state.project_id)) and ingest_record_event (UUID(project_id)) explicitly convert these values, confirming request.state stores them as raw strings. A prior review already flagged this same gap on the sibling turns handler as unresolved; it's now been copied into the newly-added complete_turn.
🛠️ Proposed fix
async def complete_turn(
self,
request: Request,
body: SessionTurnCompleteRequest,
) -> SessionTurnResponse:
- project_id: UUID = request.state.project_id
- user_id: UUID = request.state.user_id
+ project_id: UUID = UUID(request.state.project_id)
+ user_id: UUID = UUID(request.state.user_id)Plan-only workspace for removing the one-approval-per-turn latch so each gated tool call in a turn emits its own approval card, pluralizing the parked-approval record and the warm/keep-alive resume path, and verifying the already-plural frontend and SDK behavior with tests. Plans #5373, assesses #5078 and #5097. Claude-Session: https://claude.ai/code/session_01DnWRxU3dCJ11hgDidm26vW
Remove the one-approval-per-turn latch (PendingApprovalLatch) so each gated tool call in a turn emits its own approval card; pluralize the parked-approval record (a Map keyed by tool-call id) and the warm keep-alive resume input (a list of decisions), settling each parked gate by its own tool-call id on a live resume; defer orphan-sibling settling to the post-drain sweep so a concurrent gate is not force-settled before its own permission request arrives. Add runner unit tests (two cards emit, neither force-settled; multi-gate warm park + resume; deny-one-approve-one; mixed/unresumable sets stay cold), SDK vercel adapter plural egress/ingress tests, and a playground all-settled resume test. Closes #5373. Claude-Session: https://claude.ai/code/session_01DnWRxU3dCJ11hgDidm26vW
Complete the plural approval path in run-turn: record every parkable gate into env.parkedApprovals (keyed by tool-call id), iterate opts.resume.decisions to answer each parked gate by its own permissionId on the live session, defer the orphan-sibling settle to the post-drain sweep (so a concurrent gate is not force-settled before its own permission request arrives), and wire the non-parkable-pause signal. Update the acp-interactions unit test to assert both concurrent gates emit their own card. Part of the concurrent-approvals change (#5373); sits on the deny-frame egress lane whose markToolCallDenied the live-resume deny path reuses. Claude-Session: https://claude.ai/code/session_01DnWRxU3dCJ11hgDidm26vW
|
Second amendment landed: record row ids are now scoped by execution (each execution keeps its own version of an event; retries within one execution reuse their row) and the turns ledger carries the execution's turn_id, making records and turns joinable. Note for review: this commit also carries the two removed workflow-reference guards in run-turn.ts, which belong to the approvals ruling explained on #5382; GitButler's hunk attribution routes them to this base lane because the guard code predates the approvals branch. Runner 1243 and sessions API 156 green against real Postgres. |
Live verification — session ledger (turn rows, ledger completion, per-execution records, ordering)Ran on the EE dev stack, agent Local Claude (Pi harness ·
Suite baselines: runner A — turn rows at start (session
|
feat(runner): reliable multi-gate approvals: truthful results, persisted answers, per-card dispatch
One CSS-native, reduced-motion-proof collapse primitive (HeightCollapse) for everything that enters/leaves the composer region and config pane, plus the config-section header shimmer + draft/agent indicator tones on ConfigAccordionSection (and a useRecentFlag helper). Migrates RevealCollapse and AgentCommitNotice onto it so the motion language is consistent. Adds the motion dependency. (cherry picked from commit b79dbf9)
Reusable, dependency-free primitives for showing 'what changed' in a config surface: ChangedPathsContext (which dot-paths have uncommitted changes + a host-supplied revert), FocusPathsContext (narrow a surface to just the paths that matter — the same idea as rendering an input only when needed), and RailField opting into both via a path. sectionChanges maps the commit-diff classifier to the config panel's sections. Adds the draft-change and provider-key-added signal atoms the config pane consumes. (cherry picked from commit 351be6c)
…nline provider key Drawer-backed config sections (Model & harness, Advanced) surface only what needs attention inline instead of routing to the drawer: a missing provider key shows the key field right there, and uncommitted changes show the changed controls (focus-filtered via FocusPaths/ChangedPaths), each row marked and revertable, with a link out to the full drawer. Wires the sandbox/harness permission controls and the provider-credentials/key fields with their dot-paths; adds the provider-key-added notice. (cherry picked from commit 2fe28f9)
…olve Approval dock gains an 'Always allow <tool> for this agent' toggle that writes a per-tool permission (gateway/custom-function) or harness allow-rule into the draft config (toolPermission helpers), so the tool stops gating on resume and future runs; a contained notice offers Undo. Answering resolves the whole parked batch in one step — 'Approve all', or 'Approve' with the toggle on also clears that tool's sibling gates — instead of stepping through 1-of-N. (cherry picked from commit bf96673)
(cherry picked from commit 9c4ffd1)
feat(frontend): approval friction UX and context-driven config sections (Arda's port)
Regenerated the TS client from the #5436 backend OpenAPI spec so `SessionQueryRequest` carries `include_ended`/`include_archived` and `SessionStream` carries `archived_at` as first-class typed fields, and dropped the runtime cast on `querySessions` that stood in for them. Pulls in the rest of the #5436 sessions/mounts drift the checked-in client predated (SessionTurn `turn_id`, a SessionTurnComplete request, the mount file-order enum). No new tsc error signatures introduced (verified against the pre-regen baseline).
What this PR is
This is the living integration branch for the sessions work, taken over while JP is away. It carries three layers, each reviewed, tested, and live-verified before it landed:
session_statesblob is replaced by an append-only per-turn ledger (session_turns) and a per-session status row (session_streams, which also holds the session's name and description). The ledger is what lets a conversation continue natively after the live process is gone. Amendments added after live verification: the per-turn counter fix, duplicate appends answered with 409, ledger rows written at turn start and completed at turn end (so failed turns keep their trace link), turn listing ordered by turn index, the execution id (turn_id) on ledger rows and record row ids, and the resolved execution id threaded into every request consumer.How this PR is worked on
Arda continues the sessions work directly on this branch with plain git; new work lands as new commits, and the branch history is never rewritten. His handoff, the storage architecture document, and the design studies live on this branch under
docs/design/agent-workflows/projects/sessions-takeover/.When this merges
This PR merges to main when the sessions work is complete. The database migrations ship with that merge, so until then the migration files on this branch may still be edited in place; no released install has run them. The one-time upgrade effect at release: conversations started before the upgrade lose their warm continuation once and rebuild from the transcript.