Skip to content

feat(sessions): sessions integration — storage rework, reliable approvals, and approval UX#5436

Open
mmabrouk wants to merge 31 commits into
release/v0.105.8from
feat/sessions-storage-rework
Open

feat(sessions): sessions integration — storage rework, reliable approvals, and approval UX#5436
mmabrouk wants to merge 31 commits into
release/v0.105.8from
feat/sessions-storage-rework

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Jul 21, 2026

Copy link
Copy Markdown
Member

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:

  1. The session storage rework (JP's feat(api): rebase JP's session turns/streams backend onto v0.105.5 #5375 and feat(runner): rebase JP's session turns/streams runner onto the decomposed runner #5376, combined here unchanged). The mutable per-session session_states blob 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.
  2. The approvals fix train (feat(runner): reliable multi-gate approvals: truthful results, persisted answers, per-card dispatch #5382, merged into this branch). Five defects from the July 19 incident fixed: truthful results when a turn pauses, durable approval answers, per-card dispatch with partial answer sets, a Pi-batching park rule, and correct reload hydration. Verified by four live QA cycles; the evidence comments with quoted database rows are on this PR and on feat(runner): reliable multi-gate approvals: truthful results, persisted answers, per-card dispatch #5382.
  3. Arda's approval UX (feat(frontend): approval friction UX and context-driven config sections (Arda's port) #5438, merged into this branch): the context-driven config sections, the always-allow and approve-all approval flow, and a drawer fix, with his authorship preserved.

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.

mmabrouk added 6 commits July 21, 2026 11:33
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
@dosubot dosubot Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Jul 21, 2026
@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview, Comment Jul 21, 2026 4:40pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The 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
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 60.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is specific and matches the main sessions storage and UX work in the PR.
Description check ✅ Passed The description is clearly related to the sessions storage rework, approvals fixes, and UX changes in the diff.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sessions-storage-rework

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-production-d70f.up.railway.app/w
Image tag pr-5436-d046716
Status Failed
Railway logs Open logs
Logs View workflow run
Updated at 2026-07-21T16:48:22.651Z

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Add a timeout to sendHeartbeat

startAliveWatchdog awaits the first heartbeat before returning, so a stalled sessions API blocks session-owned turn start for the full fetch timeout. Add an AbortSignal timeout 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 value

Rename test function to reflect the new enum.

Since HarnessType was renamed to HarnessKind, 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.PI
sdks/python/agenta/sdk/agents/adapters/harnesses.py (1)

172-179: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Catch ValueError from coerce to display the friendly error message.

HarnessKind.coerce() will raise a ValueError if passed an invalid harness string (since it attempts to instantiate the enum). This bypasses the except KeyError block, meaning the helpful error message listing known harnesses is never shown for invalid input.

Consider catching both ValueError and KeyError to 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 exc
web/oss/src/components/SessionInspector/api.ts (1)

40-46: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Deduplicate fetchState and fetchStream.

With this change, the fetchState implementation is now completely identical to fetchStream (lines 28-34). If the name fetchState must 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

📥 Commits

Reviewing files that changed from the base of the PR and between dd19a60 and d9da538.

⛔ Files ignored due to path filters (51)
  • api/uv.lock is excluded by !**/*.lock
  • web/packages/agenta-api-client/src/generated/api/resources/mounts/client/Client.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/mounts/client/requests/MountQueryRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/sessions/client/Client.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/ArchiveSessionRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/DeleteSessionRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/FetchTurnRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SessionDetachRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SessionHeartbeatRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SessionQueryRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SessionRecordIngestRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SessionStreamCommandRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SessionStreamQueryRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SessionTurnAppendRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SessionTurnQueryRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/SetSessionStreamHeaderRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/UnarchiveSessionRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/resources/sessions/client/requests/index.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/CommandMode.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/HarnessKind.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/HarnessSessionRecord.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/Mount.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/MountCreate.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/MountQuery.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionHeartbeatResponseModel.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionHeartbeatResult.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionInteraction.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionMount.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionMountQuery.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionRecord.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionResponse.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionState.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionStateData.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionStateFlags.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionStateUpsertRequest.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionStream.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionStreamCommandResponse.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionStreamHeaderEdit.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionStreamResponse.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionStreamsResponse.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionTurn.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionTurnQuery.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionTurnResponse.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionTurnsResponse.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SessionsResponse.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SpanInput.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SpanOutput.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SpansNodeInput.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/SpansNodeOutput.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/WorkflowRequestData.ts is excluded by !**/generated/**
  • web/packages/agenta-api-client/src/generated/api/types/index.ts is excluded by !**/generated/**
📒 Files selected for processing (195)
  • api/entrypoints/routers.py
  • api/oss/databases/postgres/migrations/core/env.py
  • api/oss/databases/postgres/migrations/core_oss/versions/oss000000014_add_session_turns.py
  • api/oss/databases/postgres/migrations/core_oss/versions/oss000000015_add_session_streams_header.py
  • api/oss/databases/postgres/migrations/core_oss/versions/oss000000016_add_mounts_agent_id.py
  • api/oss/databases/postgres/migrations/core_oss/versions/oss000000017_drop_session_states.py
  • api/oss/databases/postgres/migrations/tracing_oss/versions/oss000000003_add_span_identity_columns.py
  • api/oss/databases/postgres/migrations/tracing_oss/versions/oss000000004_add_records_turn_span.py
  • api/oss/src/apis/fastapi/mounts/router.py
  • api/oss/src/apis/fastapi/mounts/utils.py
  • api/oss/src/apis/fastapi/otlp/extractors/adapters/logfire_adapter.py
  • api/oss/src/apis/fastapi/otlp/extractors/canonical_attributes.py
  • api/oss/src/apis/fastapi/otlp/extractors/span_data_builders.py
  • api/oss/src/apis/fastapi/otlp/utils/serialization.py
  • api/oss/src/apis/fastapi/sessions/models.py
  • api/oss/src/apis/fastapi/sessions/router.py
  • api/oss/src/apis/fastapi/tracing/router.py
  • api/oss/src/core/mounts/dtos.py
  • api/oss/src/core/mounts/interfaces.py
  • api/oss/src/core/mounts/service.py
  • api/oss/src/core/sessions/dtos.py
  • api/oss/src/core/sessions/interactions/dtos.py
  • api/oss/src/core/sessions/interactions/interfaces.py
  • api/oss/src/core/sessions/interactions/service.py
  • api/oss/src/core/sessions/records/dtos.py
  • api/oss/src/core/sessions/service.py
  • api/oss/src/core/sessions/states/dtos.py
  • api/oss/src/core/sessions/states/interfaces.py
  • api/oss/src/core/sessions/states/service.py
  • api/oss/src/core/sessions/streams/dtos.py
  • api/oss/src/core/sessions/streams/interfaces.py
  • api/oss/src/core/sessions/streams/runner_client.py
  • api/oss/src/core/sessions/streams/service.py
  • api/oss/src/core/sessions/turns/__init__.py
  • api/oss/src/core/sessions/turns/dtos.py
  • api/oss/src/core/sessions/turns/interfaces.py
  • api/oss/src/core/sessions/turns/service.py
  • api/oss/src/core/sessions/turns/types.py
  • api/oss/src/core/shared/dtos.py
  • api/oss/src/core/tracing/dtos.py
  • api/oss/src/core/tracing/service.py
  • api/oss/src/core/tracing/utils/filtering.py
  • api/oss/src/core/tracing/utils/trees.py
  • api/oss/src/dbs/postgres/mounts/dao.py
  • api/oss/src/dbs/postgres/mounts/dbas.py
  • api/oss/src/dbs/postgres/mounts/dbes.py
  • api/oss/src/dbs/postgres/mounts/mappings.py
  • api/oss/src/dbs/postgres/sessions/interactions/dao.py
  • api/oss/src/dbs/postgres/sessions/records/dao.py
  • api/oss/src/dbs/postgres/sessions/records/dbas.py
  • api/oss/src/dbs/postgres/sessions/records/dbes.py
  • api/oss/src/dbs/postgres/sessions/records/mappings.py
  • api/oss/src/dbs/postgres/sessions/states/dao.py
  • api/oss/src/dbs/postgres/sessions/states/dbes.py
  • api/oss/src/dbs/postgres/sessions/states/mappings.py
  • api/oss/src/dbs/postgres/sessions/streams/dao.py
  • api/oss/src/dbs/postgres/sessions/streams/dbes.py
  • api/oss/src/dbs/postgres/sessions/streams/mappings.py
  • api/oss/src/dbs/postgres/sessions/turns/__init__.py
  • api/oss/src/dbs/postgres/sessions/turns/dao.py
  • api/oss/src/dbs/postgres/sessions/turns/dbas.py
  • api/oss/src/dbs/postgres/sessions/turns/dbes.py
  • api/oss/src/dbs/postgres/sessions/turns/mappings.py
  • api/oss/src/dbs/postgres/sessions/turns/utils.py
  • api/oss/src/dbs/postgres/tracing/dao.py
  • api/oss/src/dbs/postgres/tracing/dbas.py
  • api/oss/src/dbs/postgres/tracing/dbes.py
  • api/oss/src/dbs/postgres/tracing/mappings.py
  • api/oss/src/dbs/postgres/tracing/utils.py
  • api/oss/src/utils/env.py
  • api/oss/tests/pytest/acceptance/session_states/test_harness_sessions_roundtrip.py
  • api/oss/tests/pytest/acceptance/session_states/test_session_states_basics.py
  • api/oss/tests/pytest/acceptance/sessions/__init__.py
  • api/oss/tests/pytest/acceptance/sessions/test_archive_unarchive.py
  • api/oss/tests/pytest/acceptance/sessions/test_records_ingest_contract.py
  • api/oss/tests/pytest/acceptance/sessions/test_stream_header_basics.py
  • api/oss/tests/pytest/acceptance/sessions/test_stream_header_roundtrip.py
  • api/oss/tests/pytest/unit/mounts/test_agent_id_backfill.py
  • api/oss/tests/pytest/unit/mounts/test_agent_mounts.py
  • api/oss/tests/pytest/unit/otlp/test_logfire_adapter.py
  • api/oss/tests/pytest/unit/session_states/__init__.py
  • api/oss/tests/pytest/unit/session_states/test_harness_sessions_mapping.py
  • api/oss/tests/pytest/unit/session_states/test_session_id_validation.py
  • api/oss/tests/pytest/unit/sessions/conftest.py
  • api/oss/tests/pytest/unit/sessions/test_command_matrix_inputs_data.py
  • api/oss/tests/pytest/unit/sessions/test_heartbeat_is_current_turn.py
  • api/oss/tests/pytest/unit/sessions/test_kill_runner_teardown.py
  • api/oss/tests/pytest/unit/sessions/test_mounts_agent_id_backfill.py
  • api/oss/tests/pytest/unit/sessions/test_record_ingest_endpoint.py
  • api/oss/tests/pytest/unit/sessions/test_records_mapping_upsert.py
  • api/oss/tests/pytest/unit/sessions/test_records_turn_span_dao.py
  • api/oss/tests/pytest/unit/sessions/test_runner_client_kill.py
  • api/oss/tests/pytest/unit/sessions/test_sessions_root_service.py
  • api/oss/tests/pytest/unit/sessions/test_stream_header_merge.py
  • api/oss/tests/pytest/unit/sessions/test_turns_dao.py
  • api/oss/tests/pytest/unit/sessions/test_turns_dao_conflict.py
  • api/oss/tests/pytest/unit/sessions/test_wp5_dao_fanout.py
  • api/oss/tests/pytest/unit/sessions/test_wp5_mount_teardown.py
  • api/oss/tests/pytest/unit/sessions/test_wp5_root_router.py
  • api/oss/tests/pytest/unit/tracing/test_dao_ingest_identity_columns.py
  • api/oss/tests/pytest/unit/tracing/utils/test_trees.py
  • api/pyproject.toml
  • clients/python/agenta_client/__init__.py
  • clients/python/agenta_client/mounts/client.py
  • clients/python/agenta_client/mounts/raw_client.py
  • clients/python/agenta_client/sessions/client.py
  • clients/python/agenta_client/sessions/raw_client.py
  • clients/python/agenta_client/types/__init__.py
  • clients/python/agenta_client/types/harness_kind.py
  • clients/python/agenta_client/types/harness_session_record.py
  • clients/python/agenta_client/types/mount.py
  • clients/python/agenta_client/types/mount_create.py
  • clients/python/agenta_client/types/mount_query.py
  • clients/python/agenta_client/types/session_heartbeat_result.py
  • clients/python/agenta_client/types/session_interaction.py
  • clients/python/agenta_client/types/session_mount.py
  • clients/python/agenta_client/types/session_mount_query.py
  • clients/python/agenta_client/types/session_record.py
  • clients/python/agenta_client/types/session_response.py
  • clients/python/agenta_client/types/session_state.py
  • clients/python/agenta_client/types/session_state_data.py
  • clients/python/agenta_client/types/session_state_upsert_request.py
  • clients/python/agenta_client/types/session_stream.py
  • clients/python/agenta_client/types/session_stream_command_response.py
  • clients/python/agenta_client/types/session_stream_header_edit.py
  • clients/python/agenta_client/types/session_stream_response.py
  • clients/python/agenta_client/types/session_streams_response.py
  • clients/python/agenta_client/types/session_turn.py
  • clients/python/agenta_client/types/session_turn_query.py
  • clients/python/agenta_client/types/session_turn_response.py
  • clients/python/agenta_client/types/session_turns_response.py
  • clients/python/agenta_client/types/sessions_response.py
  • clients/python/agenta_client/types/span_input.py
  • clients/python/agenta_client/types/span_output.py
  • clients/python/agenta_client/types/spans_node_input.py
  • clients/python/agenta_client/types/spans_node_output.py
  • clients/python/agenta_client/types/workflow_request_data.py
  • docs/docs/self-host/reference/01-configuration.mdx
  • hosting/docker-compose/ee/docker-compose.dev.yml
  • hosting/docker-compose/ee/docker-compose.gh.yml
  • hosting/docker-compose/oss/docker-compose.dev.yml
  • hosting/docker-compose/oss/docker-compose.gh.yml
  • hosting/kubernetes/helm/templates/api-deployment.yaml
  • sdks/python/agenta/sdk/agents/__init__.py
  • sdks/python/agenta/sdk/agents/adapters/harnesses.py
  • sdks/python/agenta/sdk/agents/adapters/local.py
  • sdks/python/agenta/sdk/agents/adapters/sandbox_agent.py
  • sdks/python/agenta/sdk/agents/dtos.py
  • sdks/python/agenta/sdk/agents/errors.py
  • sdks/python/agenta/sdk/agents/interfaces.py
  • sdks/python/agenta/sdk/agents/utils/wire.py
  • sdks/python/agenta/sdk/engines/tracing/tracing.py
  • sdks/python/agenta/sdk/middlewares/running/normalizer.py
  • sdks/python/agenta/sdk/models/tracing.py
  • sdks/python/oss/tests/pytest/integration/agents/_fake_runner_backend.py
  • sdks/python/oss/tests/pytest/integration/agents/_qa_transcripts.py
  • sdks/python/oss/tests/pytest/unit/agents/conftest.py
  • sdks/python/oss/tests/pytest/unit/agents/connections/test_dtos_model_ref.py
  • sdks/python/oss/tests/pytest/unit/agents/skills/test_skills_e2e.py
  • sdks/python/oss/tests/pytest/unit/agents/test_agent_composition_seam.py
  • sdks/python/oss/tests/pytest/unit/agents/test_dtos_capabilities_events.py
  • sdks/python/oss/tests/pytest/unit/agents/test_environment_lifecycle.py
  • sdks/python/oss/tests/pytest/unit/agents/test_harness_adapters.py
  • sdks/python/oss/tests/pytest/unit/agents/test_harness_identity.py
  • sdks/python/oss/tests/pytest/unit/agents/test_wire_contract.py
  • sdks/python/oss/tests/pytest/unit/test_batch_fold_stream_contract_routing.py
  • sdks/python/oss/tests/pytest/unit/test_invoke_real_handlers_negotiation_routing.py
  • sdks/python/oss/tests/pytest/unit/test_normalizer_passthrough.py
  • sdks/python/oss/tests/pytest/unit/test_tracing_store_agent.py
  • services/oss/tests/pytest/unit/agent/conftest.py
  • services/runner/src/engines/sandbox_agent/environment.ts
  • services/runner/src/engines/sandbox_agent/run-turn.ts
  • services/runner/src/engines/sandbox_agent/runtime-contracts.ts
  • services/runner/src/engines/sandbox_agent/sandbox-reconnect.ts
  • services/runner/src/engines/sandbox_agent/session-continuity-durable.ts
  • services/runner/src/protocol.ts
  • services/runner/src/server.ts
  • services/runner/src/sessions/alive.ts
  • services/runner/src/sessions/persist.ts
  • services/runner/src/version.ts
  • services/runner/tests/acceptance/server-contract.test.ts
  • services/runner/tests/integration/server-smoke.test.ts
  • services/runner/tests/unit/sandbox-lifecycle.test.ts
  • services/runner/tests/unit/sandbox-reconnect.test.ts
  • services/runner/tests/unit/server.test.ts
  • services/runner/tests/unit/session-alive-interrupt.test.ts
  • services/runner/tests/unit/session-alive.test.ts
  • services/runner/tests/unit/session-continuity-durable.test.ts
  • services/runner/tests/unit/session-keepalive-engine.test.ts
  • services/runner/tests/unit/session-persist.test.ts
  • web/oss/src/components/SessionInspector/api.ts
  • web/packages/agenta-entities/src/session/api/api.ts
  • web/packages/agenta-entities/src/session/core/liveness.ts
  • web/packages/agenta-entities/src/session/core/schema.ts
  • web/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

Comment on lines +1076 to +1102
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,
),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.py

Repository: 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
fi

Repository: 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.py

Repository: 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.

Comment on lines 279 to +304
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.py

Repository: 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.py

Repository: 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.

Comment thread clients/python/agenta_client/__init__.py
Comment on lines +18 to +20
name: typing.Optional[str] = None
description: typing.Optional[str] = None
id: typing.Optional[str] = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.py

Repository: 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 -B3

Repository: 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.

Comment on lines +278 to +280
if agent_id:
span.set_attribute("id", agent_id, namespace="agent")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Suggested change
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)

@mmabrouk

Copy link
Copy Markdown
Member Author

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
services/runner/src/engines/sandbox_agent/run-turn.ts (1)

613-613: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the existing continuityStore variable.

You can reuse the continuityStore variable 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 win

Test 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_index order (0→1→2→3), matching insertion order. This doesn't actually validate that latest_turn/latest_turn_per_harness_kind rely on ORDER BY turn_index DESC rather than insertion order — unlike test_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

📥 Commits

Reviewing files that changed from the base of the PR and between d9da538 and fab413d.

📒 Files selected for processing (13)
  • api/oss/src/apis/fastapi/sessions/models.py
  • api/oss/src/apis/fastapi/sessions/router.py
  • api/oss/src/core/sessions/turns/dtos.py
  • api/oss/src/core/sessions/turns/interfaces.py
  • api/oss/src/core/sessions/turns/service.py
  • api/oss/src/core/sessions/turns/types.py
  • api/oss/src/dbs/postgres/sessions/turns/dao.py
  • api/oss/tests/pytest/unit/sessions/test_turns_complete.py
  • api/oss/tests/pytest/unit/sessions/test_turns_dao.py
  • services/runner/src/engines/sandbox_agent/run-turn.ts
  • services/runner/src/engines/sandbox_agent/session-continuity-durable.ts
  • services/runner/tests/unit/session-continuity-durable.test.ts
  • services/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

Comment on lines +1116 to +1148
@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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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)

mmabrouk added 8 commits July 21, 2026 17:04
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
@mmabrouk

Copy link
Copy Markdown
Member Author

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.

@mmabrouk

Copy link
Copy Markdown
Member Author

Live verification — session ledger (turn rows, ledger completion, per-execution records, ordering)

Ran on the EE dev stack, agent Local Claude (Pi harness · anthropic/claude-sonnet-4-5 · local sandbox · Bash gated ask). Fresh session ids, driven through the product /services/agent/v0/invoke path; rows quoted straight from Postgres.

Note: behavior A initially failed here — the session_turns.turn_id column stayed NULL on every turn because the runner's ledger append read request.turnId, which no caller populated (the resolved execution id was minted for the alive-lock/records/interactions but never written back onto the request). Fixed in commit d5155fda56 (thread the resolved execution id onto the request at both dispatch entry points, mint when absent, dispatch-seam test pinning it). All four behaviors below pass on the fixed tree.

Suite baselines: runner pnpm test 1244 passed / pnpm run typecheck clean; API pytest oss/tests/pytest/unit/sessions/ 156 passed.


A — turn rows at start (session qa-AB-1784648240)

Queried mid-turn (while parked awaiting approval). Row exists at turn_index 0 with start_time, trace_id, non-NULL turn_id, and end_time NULL:

turn_index       | 0
turn_id          | 0a9b7e0c-dd15-47b8-90d3-cbe3ab9aa3aa
trace_id         | d4f20da5-c5c5-ba8b-4dd4-77e25bfe0ea9
span_id          | 91ec4a49635d86d7
start_time       | 2026-07-21 15:37:23.972+00
end_time_null    | t
agent_session_id | 019f8553-1432-7f3e-bc0f-307865270ef7

The turn_id matches the execution id in the runner stream log:

[sessions] stream sessionOwned=true sessionId=qa-AB-1784648240 turnId=0a9b7e0c-dd15-47b8-90d3-cbe3ab9aa3aa cred=present

After the turn completes — end_time and agent_session_id set (same turn_id):

turn_index=0  turn_id=0a9b7e0c-dd15-47b8-90d3-cbe3ab9aa3aa  end_time=2026-07-21 15:37:30.807+00  agent_session_id=019f8553-1432-7f3e-bc0f-307865270ef7

PASS.


B — ledger completion across an approval pause (same session)

Single gated write, parked then approved (warm resume within TTL). The turn spans two executions but leaves exactly one ledger row, completed:

rows | 1
turn_index=0  end_time=2026-07-21 15:37:30.807+00  agent_session_id=019f8553-...

The resume's duplicate start-insert is handled as a benign 409 (API access log), and the runner completes the same turn 0:

runner: [keepalive] resume key=...:qa-AB-1784648240 gates=1 answered=1 carried=0 approve=1 reject=0 tool=Bash
runner: [sandbox-agent] complete OK session=qa-AB-1784648240 turn=0
api:    POST /api/sessions/turns/ HTTP/1.1 409

PASS.


C — two record versions across executions (same session, tracing DB records)

The same gated tool call id toolu_01RM3NGdzpHCr82aNrz8ujSF produces two records with different record ids and different turn_ids, one per execution (record ids scoped by execution):

record_id    | d7dd6f74-fb45-51bc-b278-a0816bd7f445      <- execution 1 (pre-approval placeholder)
record_type  | tool_call
turn_id      | 0a9b7e0c-dd15-47b8-90d3-cbe3ab9aa3aa
attributes   | {"id":"toolu_01RM3NGdzpHCr82aNrz8ujSF","name":"bash","type":"tool_call","input":{"command":"echo WROTE-474c7b > /tmp/qa-474c7b.txt && cat /tmp/qa-474c7b.txt"}}

record_id    | 429fa8da-db95-5a50-83bb-000a0eefb643      <- execution 2 (re-emitted call)
record_type  | tool_call
turn_id      | b915dde4-bd59-4574-8613-0177b340db77
attributes   | {"id":"toolu_01RM3NGdzpHCr82aNrz8ujSF","name":"Bash","type":"tool_call",...}

record_id    | 608ddfaa-ebfd-5dbf-9f03-48e990a5c5c7      <- execution 2 real output
record_type  | tool_result
turn_id      | b915dde4-bd59-4574-8613-0177b340db77
attributes   | {"id":"toolu_01RM3NGdzpHCr82aNrz8ujSF","type":"tool_result","output":"WROTE-474c7b\n","isError":false}

Same tool-call id, two turn_ids, two distinct record ids — the pre-approval placeholder and the executed output no longer collide. PASS.


E — ordering by turn_index (session qa-E-1784648365, 4 turns)

POST /api/sessions/turns/query (ascending) returns rows ordered by turn_index, not the old span_id cursor:

order: [0, 1, 2, 3]

turn_index=0  turn_id=1a06688b-ff8b-4490-86f5-df3319f9ddfc  start=15:39:28.774  end=15:39:31.291
turn_index=1  turn_id=49ac4094-ef8f-4f9a-bdb3-d7203585277c  start=15:39:33.968  end=15:39:36.438
turn_index=2  turn_id=61dcbb0d-28bd-4c76-baa2-b39ad6cb7cbd  start=15:39:39.090  end=15:39:41.283
turn_index=3  turn_id=c57a14a1-33db-4b4e-b080-f502e17200e8  start=15:39:43.961  end=15:39:46.120

Every turn carries a distinct non-NULL turn_id. PASS.

@mmabrouk
mmabrouk changed the base branch from main to release/v0.105.8 July 21, 2026 16:03
mmabrouk and others added 6 commits July 21, 2026 17:15
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)
feat(frontend): approval friction UX and context-driven config sections (Arda's port)
@mmabrouk mmabrouk changed the title feat(sessions): append-only session turns and streams storage rework feat(sessions): sessions integration — storage rework, reliable approvals, and approval UX Jul 21, 2026
ardaerzin added a commit that referenced this pull request Jul 21, 2026
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend Feature Request New feature or request needs-review Agent updated; awaiting Mahmoud's review size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants