Skip to content

feat(sessions): separate session history from tracing retention and gate immutable record writes - #6517

Closed
mmabrouk wants to merge 5 commits into
mainfrom
feat/session-history-producer
Closed

feat(sessions): separate session history from tracing retention and gate immutable record writes#6517
mmabrouk wants to merge 5 commits into
mainfrom
feat/session-history-producer

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Sep 3, 2026

Copy link
Copy Markdown
Member

Session records are conversation history, but today they sit under the tracing quota and retention: the records worker drops an over-quota org's records and marks nothing, so history can vanish silently. This PR is increment 3 of the session-control design (docs/design/session-control-and-live-events on PR #6495): the durable-history producer work that needs no client change.

What changes

  • Session records are exempt from the tracing quota drop in OSS and EE, and from tracing retention. A new session-scoped retention setting lives in env.py. When a record is still lost, session_streams.history_incomplete is set (additive nullable column, migration oss000000025; it was 22, renumbered because PR feat(sessions): deliver durable Stop directly to the runner #6503 uses 22 on the same parent; whichever PR lands second re-points down_revision to the new head).
  • Behind AGENTA_SESSIONS_HISTORY_WRITES (default off): stable record ids that a retry reuses, one open slot per tool id so a completed tool call is durable before the turn ends, and per-record rejection of a retry that carries different content under the same id. The rejection is a typed core exception with code record_conflict, 409 at the router. Other records in the batch still commit.
  • The runner sends a producer_id as an additive field that an older API ignores; with the flag off the API path is unchanged.

Not in this PR: the per-session sequence (open question O1 in the design), snapshot, replay.

Tests

  • pytest oss/tests/pytest/unit/sessions -q against Postgres: 539 passed.
  • pnpm test in services/runner: 2,688 passed; session-persist.test.ts: 28 passed.
  • Reviewed twice by an Opus agent (round 2 verdict: ship). One known leftover: the worker still acknowledges a batch whose transaction failed because processed_ids is filled at deserialization; pre-existing, tracked in the durable-history package.

Known items for review

  • The 409 mapping cannot be reached today because the ingest route only writes to Redis; the worker path raises the typed exception.
  • With the retention setting unset, no session record is ever deleted. That is the intended default for now; say if you want a bound.
  • The column is history_incomplete; the design contract names the snapshot field history_complete. One of the two should flip before the snapshot ships.

Agent-generated, low weight. Not merged.

https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV

@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
agenta-documentation Ready Ready Preview Sep 4, 2026 10:07am UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Summary

Summary by CodeRabbit

  • New Features

    • Added optional immutable session-history writes with stable retry identifiers.
    • Duplicate records with changed content are now reported as conflicts instead of being silently overwritten.
    • Record-ingestion conflicts return a clear HTTP 409 response.
    • Session histories can be marked as incomplete when records cannot be recovered.
    • Added configurable session-history retention and write settings.
    • Improved tool-event persistence across multiple concurrent tool calls.
  • Bug Fixes

    • Failed record writes remain eligible for retry, while successful and recognized conflicts are acknowledged.

Walkthrough

The change adds immutable session-record writes with stable producer IDs, typed content conflicts, updated worker acknowledgment behavior, permanent incomplete-history markers, revised runner checkpointing, and global session-level retention.

Changes

Session history durability

Layer / File(s) Summary
History contracts and incomplete markers
api/oss/src/core/sessions/{records,streams}/*, api/oss/src/dbs/postgres/sessions/streams/*, api/oss/databases/postgres/migrations/..., api/oss/src/utils/env.py
Adds conflict types, append results, producer IDs, incomplete-history fields, DAO contracts, the migration, and history settings.
Immutable writes and conflict responses
api/oss/src/dbs/postgres/sessions/records/dao.py, api/oss/src/core/sessions/records/service.py, api/oss/src/apis/fastapi/sessions/*, api/oss/tests/pytest/unit/sessions/test_records_history_writes.py, test_record_ingest_endpoint.py, test_orphaned_gate_reconciliation.py, test_records_history_durability.py
Adds immutable upsert behavior, conflict detection, result normalization, conflict logging, and HTTP 409 mapping.
Worker acknowledgment and history marking
api/entrypoints/worker_streams.py, api/oss/src/tasks/asyncio/sessions/records_worker.py, api/oss/src/dbs/postgres/sessions/streams/dao.py, api/oss/tests/pytest/unit/sessions/test_records_history_durability.py, test_records_worker_batching.py, test_watch_publish.py
Acknowledges committed or rejected stable IDs, retries failed batches per record, reconciles committed events, publishes session changes, and updates incomplete-history markers.
Runner checkpointing and stable retry IDs
services/runner/src/sessions/persist.ts, services/runner/src/sessions/record-id.ts, services/runner/tests/unit/session-persist.test.ts
Stages tool events per tool ID, removes TTL flushing, assigns stable producer IDs, and preserves retry request bodies.
Session-level record retention
api/ee/src/core/sessions/records/service.py, api/ee/src/dbs/postgres/sessions/records/dao.py, api/oss/tests/pytest/unit/sessions/test_records_history_durability.py
Uses the global session retention setting and deletes expired records without plan-based filtering.

Estimated code review effort: 5 (Critical) | ~120 minutes

Merge Risk: 🟡 Moderate · up to 8c7af

Retry and conflict edge cases can duplicate session history or leave committed records without required follow-up processing. These durability issues should be fixed before merge.

Sequence Diagram(s)

sequenceDiagram
  participant Runner
  participant IngestAPI
  participant RecordsWorker
  participant RecordsDAO
  participant SessionStreamsDAO
  Runner->>IngestAPI: send record with producer_id
  IngestAPI->>RecordsDAO: append record
  RecordsDAO-->>IngestAPI: committed record or content conflict
  RecordsWorker->>RecordsDAO: append queued records
  RecordsDAO-->>RecordsWorker: append result with conflicts
  RecordsWorker->>SessionStreamsDAO: mark incomplete history when required
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.66% which is insufficient. The required threshold is 60.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 83 functions across 28 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: separating session history retention from tracing retention and controlling immutable record writes.
Description check ✅ Passed The description directly explains the session history retention, immutable writes, conflict handling, migrations, compatibility behavior, tests, and known limitations in the changeset.
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.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/session-history-producer

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.

…on id

PR #6503 already uses oss000000022 on the same parent. This file becomes oss000000025 so
both branches can merge; whichever lands second re-points down_revision to the new head.

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
mmabrouk added a commit that referenced this pull request Sep 4, 2026
PR #6517 already uses oss000000025 on this chain. This file becomes oss000000026 on top of 024.

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
mmabrouk added a commit that referenced this pull request Sep 4, 2026
Re-point oss000000025 onto oss000000026 so the core chain is 024 -> 026 -> 025.

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
@mmabrouk
mmabrouk marked this pull request as ready for review September 4, 2026 15:43
@mmabrouk mmabrouk closed this Sep 4, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
api/oss/src/core/sessions/records/service.py (1)

92-103: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Return a Pydantic DTO from RecordsService.mark_history_incomplete.

The API convention requires service methods to return Pydantic BaseModel DTOs. This method forwards the int returned by SessionStreamsDAOInterface, and no current caller uses the result. If the method remains, return a DTO containing the count; otherwise remove the unused method.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Team

Run ID: 3a934d4d-6884-47e1-827d-d53d72575f55

📥 Commits

Reviewing files that changed from the base of the PR and between ded2cc3 and 8c7af69.

📒 Files selected for processing (28)
  • api/ee/src/core/sessions/records/service.py
  • api/ee/src/dbs/postgres/sessions/records/dao.py
  • api/entrypoints/worker_streams.py
  • api/oss/databases/postgres/migrations/core_oss/versions/oss000000025_add_session_history_incomplete.py
  • api/oss/src/apis/fastapi/sessions/models.py
  • api/oss/src/apis/fastapi/sessions/router.py
  • api/oss/src/core/sessions/records/dtos.py
  • api/oss/src/core/sessions/records/interfaces.py
  • api/oss/src/core/sessions/records/service.py
  • api/oss/src/core/sessions/records/types.py
  • api/oss/src/core/sessions/streams/dtos.py
  • api/oss/src/core/sessions/streams/interfaces.py
  • api/oss/src/dbs/postgres/sessions/records/dao.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/tasks/asyncio/sessions/records_worker.py
  • api/oss/src/utils/env.py
  • api/oss/tests/pytest/unit/migrations/test_single_heads.py
  • api/oss/tests/pytest/unit/sessions/test_orphaned_gate_reconciliation.py
  • api/oss/tests/pytest/unit/sessions/test_record_ingest_endpoint.py
  • api/oss/tests/pytest/unit/sessions/test_records_history_durability.py
  • api/oss/tests/pytest/unit/sessions/test_records_history_writes.py
  • api/oss/tests/pytest/unit/sessions/test_records_worker_batching.py
  • api/oss/tests/pytest/unit/sessions/test_watch_publish.py
  • services/runner/src/sessions/persist.ts
  • services/runner/src/sessions/record-id.ts
  • services/runner/tests/unit/session-persist.test.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.

Comment on lines +220 to +235
if len(entries) > 1:
for entry in entries:
(
appended,
committed_ids,
committed_events,
) = await self._append_one(
project_id=project_batch["project_id"],
entry=entry,
)
total_appended += appended
acknowledged_ids.extend(committed_ids)
await self._after_commit(
project_id=project_batch["project_id"],
events=committed_events,
)

Copy link
Copy Markdown
Contributor

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

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline api/oss/src/dbs/postgres/sessions/records/dao.py --items all --type function --match 'append|append_many'

rg -n -C 12 \
  'async def append_many|async def append|AGENTA_SESSIONS_HISTORY_WRITES|commit\(|rollback\(|record_id|producer_id|RecordContentConflict' \
  api/oss/src/dbs/postgres/sessions/records/dao.py \
  api/oss/src/core/sessions/records/interfaces.py \
  api/oss/src/core/sessions/records/dtos.py

Repository: Agenta-AI/agenta

Length of output: 31964


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- worker outline and relevant source ---'
ast-grep outline api/oss/src/tasks/asyncio/sessions/records_worker.py --items all --type function
rg -n -C 18 \
  'append_many|_append_one|except|AGENTA_SESSIONS_HISTORY_WRITES|producer_id|record_id|RecordsService' \
  api/oss/src/tasks/asyncio/sessions/records_worker.py

printf '%s\n' '--- service bindings ---'
fd -i -t f 'service' api/oss/src/core/sessions/records api/oss/src/tasks/asyncio/sessions
rg -n -C 12 \
  'async def append_many|async def append|append_many\(|records_dao|RecordsDAOInterface' \
  api/oss/src/core/sessions/records api/oss/src/tasks/asyncio/sessions

printf '%s\n' '--- database entity/schema ---'
fd -i -t f 'dbe|model|record' api/oss/src/dbs/postgres/sessions/records
rg -n -C 16 \
  'class RecordDBE|record_id|UniqueConstraint|primary_key|server_default' \
  api/oss/src/dbs/postgres/sessions/records

Repository: Agenta-AI/agenta

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 18 \
  'append_many|_append_one|except|AGENTA_SESSIONS_HISTORY_WRITES|producer_id|record_id|RecordsService' \
  api/oss/src/tasks/asyncio/sessions/records_worker.py

rg -n -C 12 \
  'async def append_many|async def append|append_many\(|records_dao|RecordsDAOInterface' \
  api/oss/src/core/sessions/records api/oss/src/tasks/asyncio/sessions

rg -n -C 16 \
  'class RecordDBE|record_id|UniqueConstraint|primary_key|server_default' \
  api/oss/src/dbs/postgres/sessions/records

Repository: Agenta-AI/agenta

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- history_writes default ---'
rg -n -C 8 \
  'history_writes|AGENTA_SESSIONS_HISTORY_WRITES' \
  api/oss/src

printf '%s\n' '--- record-event construction and serialization ---'
rg -n -C 8 \
  'SessionRecordEvent\(|record_event|producer_id|record_id' \
  services api/oss/src 2>/dev/null | head -n 500

Repository: Agenta-AI/agenta

Length of output: 49637


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 \
  'history_writes|AGENTA_SESSIONS_HISTORY_WRITES' \
  api/oss/src

rg -n -C 8 \
  'SessionRecordEvent\(|record_event|producer_id|record_id' \
  services api/oss/src 2>/dev/null | head -n 500

Repository: Agenta-AI/agenta

Length of output: 47741


Make replay idempotent in legacy mode. When AGENTA_SESSIONS_HISTORY_WRITES=false and record_id is absent, RecordsDAO ignores producer_id and generates a new uuid4 for each append_many call. If the initial commit succeeds but raises before process_batch receives success, this loop can retry each entry with a different key and insert duplicate rows. Use a retry-stable key in legacy mode or avoid replay after an uncertain commit.

Comment on lines +239 to +245
conflicts = set(result.conflicting_record_ids)
committed_events = [
msg
for _, msg in entries
if (msg.record_event.record_id or msg.record_event.producer_id)
not in conflicts
]

Copy link
Copy Markdown
Contributor

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

Derive committed events from result.records

When immutable history writes are enabled, RecordsDAO._dedupe_immutable_values keeps the first event for a stable ID and reports the later differing event in conflicting_record_ids. The worker then excludes every event with that ID, including the event that append_many committed. This leaves committed_events empty, so _after_commit skips gate reconciliation and records_changed publication. Use the returned records to select the accepted event once per ID, or return per-entry outcomes. Add a regression test for two same-ID events with different content.

mmabrouk added a commit that referenced this pull request Sep 4, 2026
PR #6517 already uses oss000000025 on this chain. This file becomes oss000000026 on top of 024.

Claude-Session: https://claude.ai/code/session_01GAqSs7fw6QRi2n1ZJ2tmAV
@mmabrouk

mmabrouk commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

🤖 The AI agent says:

Suggestion (non-blocking, if this closed PR is revived): remove the unreachable HTTP conflict handler.

This proposal combines history retention with gated immutable record writes. The PR body already notes that the HTTP 409 cannot be reached: ingest_record_event awaits Redis publication, while RecordContentConflict is raised later by the Postgres worker. The try/except around publish_record therefore suggests a synchronous conflict contract this endpoint cannot provide.

I would delete that handler and describe ingestion as asynchronous acceptance, retaining immutable-write enforcement and conflict reporting in the worker. That removes misleading API behavior without adding a synchronous database write just to make the catch reachable. If callers actually require an immediate conflict verdict, that is a separate API contract decision.

Static simplification review of the handler at the reviewed head. No implementation or live tests were run for this suggestion.

@mmabrouk

mmabrouk commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

Agent-generated, low weight. This PR is out of scope for the release and is being closed, so the unreachable branch goes with it.

@mmabrouk

mmabrouk commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

🤖 The AI agent says:

[P2] Correctness, if this closed proposal is revived: mark retained sessions incomplete before deleting their history.

When AGENTA_SESSIONS_HISTORY_RETENTION_DAYS is configured, flush_records deletes expired records but never updates the affected sessions' new history_incomplete marker. For a session containing both old and recent turns, the next retention pass removes its prefix while the stream still reports the default complete-history state. This defeats the marker for the very retention loss this change makes configurable; snapshot/reconstruction consumers cannot distinguish the remaining suffix from complete history.

Reuse mark_history_incomplete for sessions selected for deletion, and only delete after that marker is durable. The default unlimited-retention path can stay unchanged. Cover a session with one expired and one retained record.

Source: retention deletion loop. Confirmed by static tracing of the retention service/DAO and marker writers at 8c7af6954e564224c325ee2d6f9a1c1ce17a2b28; no retention database test was run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant