Skip to content

feat: State persistence recovery fix - #113

Open
saharannaveen wants to merge 26 commits into
redhat-data-and-ai:deep-agentfrom
saharannaveen:feat/rhitaif-206-state-persistence
Open

feat: State persistence recovery fix#113
saharannaveen wants to merge 26 commits into
redhat-data-and-ai:deep-agentfrom
saharannaveen:feat/rhitaif-206-state-persistence

Conversation

@saharannaveen

Copy link
Copy Markdown

Summary

Descrition
When an agent pod is killed mid-conversation (rolling update, OOM, node eviction), the in-flight run is lost — the user sees
a frozen chat with no response, and the conversation cannot be resumed. There is no mechanism to track which runs were
active, persist their state, or recover them on a replacement pod.

Changes

template-agent (14 commits, 2680 lines added)

New module: deep_agent/aegra/lifecycle.py — Lifecycle state persistence engine that tracks inflight runs in Redis and
recovers them after pod restarts:

  • register_inflight — records run metadata (thread_id, checkpoint_id, user tokens) in Redis when a run starts, with a
    TTL-based lease
  • update_inflight_checkpoint — updates checkpoint progress on each graph node completion
  • deregister_inflight — removes the Redis key when a run finishes or fails
  • persist_inflight_runs — called during graceful shutdown (SIGTERM), marks all local inflight runs as interrupted in both
    Redis and Postgres
  • resume_interrupted_runs — called during startup, picks up interrupted runs using FOR UPDATE SKIP LOCKED (prevents
    duplicate processing across replicas), claims them via lease, and re-enqueues through the Aegra worker executor
  • Token encryption via Fernet (derived from SSO_CLIENT_SECRET) for sensitive SSO tokens stored in Redis inflight records

Modified: deep_agent/aegra/startup.py — Calls resume_interrupted_runs on pod startup to recover any runs left behind by a
killed predecessor

Modified: deep_agent/aegra/shutdown.py — Calls persist_inflight_runs during graceful shutdown to mark active runs as
interrupted before the pod terminates

Modified: deep_agent/aegra/graph.py — Integrates lifecycle tracking into the agent graph: registers inflight at run start,
updates checkpoint on node completion, deregisters on completion/failure

Testing: 641-line unit test suite (test_lifecycle.py) + 318-line integration tests + 3 shell scripts for Kind cluster
pod-kill testing (kind-pod-kill-test.sh, pod-kill-and-recover.sh, inspect-db.sh)

User Flow

  1. User sends a message in the chat UI — the agent starts processing.
  2. The agent pod is killed mid-run (rolling update, OOM, kubectl delete pod).
  3. Shutdown hook fires: persist_inflight_runs() marks the active run as interrupted in Redis + Postgres with the latest
    checkpoint_id.
  4. The SSE stream drops — the UI detects the error and starts recovery polling (every 5s).
  5. A replacement pod starts up and calls resume_interrupted_runs().
  6. The new pod claims the interrupted run (lease-based, FOR UPDATE SKIP LOCKED), loads the checkpoint from Postgres, and
    resumes the graph from the last completed node.
  7. The run completes on the new pod — the final response is written to the thread state.
  8. The UI's recovery poller detects the completed thread state and renders the response — the user sees the answer as if
    nothing happened.

@saharannaveen
saharannaveen requested a review from a team as a code owner July 22, 2026 08:52
@nirmchan

Copy link
Copy Markdown

Hi @saharannaveen There are some minor issues have given inline review comments kindly check /address also the CI is failing with some issues, may need to be resolved. LGTM otherwise

@saharannaveen
saharannaveen force-pushed the feat/rhitaif-206-state-persistence branch from d836d18 to fa37166 Compare July 28, 2026 07:40
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds optional lifecycle persistence for compiled graphs. Redis tracks inflight runs and checkpoints. PostgreSQL stores leases, interruption metadata, and recovery status. Shutdown persists active runs, while startup initializes Aegra and resumes eligible runs. New settings control persistence and recovery. Tests cover encryption, Redis tracking, lease recovery, startup behavior, and pod coordination. The mock MCP server now uses FastMCP Streamable HTTP.

Suggested reviewers: np-compete, vishnusrichand

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.71% which is insufficient. The required threshold is 80.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 clearly identifies the primary change: state persistence and recovery for interrupted runs.
Description check ✅ Passed The description directly explains lifecycle persistence, interrupted-run recovery, startup and shutdown integration, and related tests.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
🚀 Post-Merge Actions
  • Update changelog

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

@codecov-commenter

codecov-commenter commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 58.13008% with 103 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
deep_agent/aegra/startup.py 34.61% 51 Missing ⚠️
deep_agent/aegra/lifecycle.py 73.72% 36 Missing ⚠️
deep_agent/aegra/graph.py 20.00% 12 Missing ⚠️
deep_agent/aegra/shutdown.py 63.63% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@saharannaveen saharannaveen changed the title State persistence recovery feat: State persistence recovery Jul 28, 2026
@nirmchan nirmchan linked an issue Jul 28, 2026 that may be closed by this pull request
6 tasks
@saharannaveen
saharannaveen force-pushed the feat/rhitaif-206-state-persistence branch from af71484 to 500468f Compare July 28, 2026 08:14
@NP-compete

Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@deep_agent/aegra/lifecycle.py`:
- Around line 445-530: Refactor persist_inflight_runs to expire all collected
run_ids with one database UPDATE using a run_id = ANY(...) filter, rather than
calling _expire_run_lease once per run. Preserve the existing running-status
condition, lease metadata updates, logging, deregistration, and persisted-count
behavior; update _expire_run_lease or replace its usage so only a single
synchronous Postgres connection is opened during shutdown.
- Around line 343-369: Update get_inflight_run_ids to batch the per-key hash
reads through a Redis pipeline rather than calling hgetall inside the loop.
Preserve filtering for the current POD_ID, collection of non-empty run_id
values, and the existing unavailable/error fallback behavior.

In `@deep_agent/aegra/startup.py`:
- Around line 160-172: Cap the interrupted-run recovery query in the startup
recovery flow by adding the same 20-row limit used by the stale-run diagnostic
query. Keep the recovered_run_ids collection and subsequent UPDATE behavior
unchanged, while ensuring the SELECT does not load more than 20 rows.

In `@scripts/inspect-db.sh`:
- Around line 55-68: Replace direct $THREAD_ID interpolation in the thread and
forensics SQL commands with psql variable binding, passing the identifier
through the psql invocation and referencing the bound variable in each query.
Apply the same protection to the additional affected query range while
preserving the existing query behavior and output.

In `@scripts/kind-pod-kill-test.sh`:
- Line 33: Replace the hardcoded username and password in the DB connection
string with values sourced from environment variables or the project’s existing
secret configuration, while preserving the current host, port, and database
name.

In `@scripts/pod-kill-and-recover.sh`:
- Line 104: Update the background Aegra command in the pod recovery script to
write logs to a securely created temporary file using mktemp, rather than the
predictable /tmp/agent.log path. Reuse the generated temporary filename for
output redirection and preserve the existing background startup behavior.

In `@tests/integration/test_lifecycle_integration.py`:
- Around line 30-75: Update the _FakeRedis test double to implement scan_iter
with the same prefix-pattern behavior as keys, yielding matching keys after
cleaning expired entries. Ensure get_inflight_run_ids can scan this mock
successfully so lifecycle assertions validate actual returned run IDs rather
than the module’s exception fallback.

In `@tests/unit/test_lifecycle.py`:
- Around line 465-476: Add pytest asyncio markers to test_no_interrupted_runs
and every other async test in TestResumeInterruptedRuns in
tests/unit/test_lifecycle.py (anchor site, lines 465-476), and to
test_resume_after_persist and test_stale_lease_reclaimed in
tests/integration/test_lifecycle_integration.py (sibling site, lines 177-206).
Alternatively, confirm the project config enables asyncio_mode = auto; do not
leave these async tests unmarked when that setting is unavailable.
- Around line 87-97: Replace the mocked `_get_encryption_secret` call in
`test_encryption_secret_uses_sso_client_secret` with an invocation of the real
`lifecycle._get_encryption_secret` function while retaining the patched settings
containing `SSO_CLIENT_SECRET`; assert that it returns the configured secret so
the test exercises the implementation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f1d52010-19bc-42a7-99ba-13a4b01ae37a

📥 Commits

Reviewing files that changed from the base of the PR and between 64c6065 and 00ea95e.

📒 Files selected for processing (14)
  • deep_agent/aegra/graph.py
  • deep_agent/aegra/lifecycle.py
  • deep_agent/aegra/shutdown.py
  • deep_agent/aegra/startup.py
  • deep_agent/src/infrastructure/subagents.py
  • deep_agent/src/settings.py
  • scripts/inspect-db.sh
  • scripts/kind-pod-kill-test.sh
  • scripts/pod-kill-and-recover.sh
  • tests/integration/conftest.py
  • tests/integration/test_lifecycle_integration.py
  • tests/mocks/mock_mcp_server.py
  • tests/unit/aegra/test_graph.py
  • tests/unit/test_lifecycle.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • redhat-data-and-ai/template-mcp (manual)
  • redhat-data-and-ai/template-ui (manual)

Comment thread deep_agent/aegra/lifecycle.py Outdated
Comment thread deep_agent/aegra/lifecycle.py Outdated
Comment thread deep_agent/aegra/startup.py Outdated
Comment thread scripts/inspect-db.sh Outdated
Comment thread scripts/kind-pod-kill-test.sh Outdated
Comment thread scripts/pod-kill-and-recover.sh Outdated
Comment thread tests/integration/test_lifecycle_integration.py Outdated
Comment thread tests/unit/test_lifecycle.py Outdated
Comment thread tests/unit/test_lifecycle.py
@saharannaveen

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
scripts/inspect-db.sh (1)

102-108: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Compare the claimant with the original worker before labeling recovery, because a non-null, non-dash claimed_by only proves that a run is claimed and mislabels ordinary single-worker runs as recovered by a different pod.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/inspect-db.sh` around lines 102 - 108, Update the “WORKER POD
HISTORY” query in the runs case to compare claimed_by with the run’s original
worker identity, and only include runs where those values differ; retain the
existing non-null/non-dash filters and recovered-by-pod output.
scripts/kind-pod-kill-test.sh (1)

172-192: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fail when claimed_by does not match a running pod instead of killing the first pod, because the fallback can terminate an unrelated replica and leave the target worker alive.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/kind-pod-kill-test.sh` around lines 172 - 192, Update the KILL
selection logic around HANDLING_POD and POD_TO_KILL so that no fallback pod is
selected when no running pod matches the claimed handler. Fail the script with
an error and stop before kubectl delete, ensuring only the explicitly matched
pod can be terminated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@deep_agent/aegra/lifecycle.py`:
- Around line 361-364: Update the pipeline flow around client.pipeline() and
pipeline.execute() to process keys in bounded batches instead of queuing every
scanned key at once. Execute each batch, collect or handle its results
incrementally, and ensure no unbounded list of keys or hashes is materialized
while preserving the existing hgetall behavior.

In `@scripts/inspect-db.sh`:
- Around line 177-182: Update the checkpoint aggregation query in the $PG
invocation to cast metadata::jsonb->>'step' to a numeric type before applying
MIN and MAX, so from_step and to_step use numeric ordering while preserving the
existing run and count aggregation.

---

Outside diff comments:
In `@scripts/inspect-db.sh`:
- Around line 102-108: Update the “WORKER POD HISTORY” query in the runs case to
compare claimed_by with the run’s original worker identity, and only include
runs where those values differ; retain the existing non-null/non-dash filters
and recovered-by-pod output.

In `@scripts/kind-pod-kill-test.sh`:
- Around line 172-192: Update the KILL selection logic around HANDLING_POD and
POD_TO_KILL so that no fallback pod is selected when no running pod matches the
claimed handler. Fail the script with an error and stop before kubectl delete,
ensuring only the explicitly matched pod can be terminated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d1722fd1-eec6-4c63-9bbe-4d12e1bb8cb0

📥 Commits

Reviewing files that changed from the base of the PR and between 00ea95e and 46bde3d.

📒 Files selected for processing (7)
  • deep_agent/aegra/lifecycle.py
  • deep_agent/aegra/startup.py
  • scripts/inspect-db.sh
  • scripts/kind-pod-kill-test.sh
  • scripts/pod-kill-and-recover.sh
  • tests/integration/test_lifecycle_integration.py
  • tests/unit/test_lifecycle.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • redhat-data-and-ai/template-mcp (manual)
  • redhat-data-and-ai/template-ui (manual)

Comment thread deep_agent/aegra/lifecycle.py Outdated
Comment thread scripts/inspect-db.sh Outdated
@saharannaveen

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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.

♻️ Duplicate comments (1)
deep_agent/aegra/lifecycle.py (1)

361-364: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Avoid unbounded key materialization

As per path instructions, the 50-key pipelines still rely on keys = list(client.scan_iter(...)), which retains every matching key in memory; consume the iterator incrementally into bounded batches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deep_agent/aegra/lifecycle.py` around lines 361 - 364, Update the
key-processing flow around the batch_size loop to consume client.scan_iter(...)
incrementally instead of materializing all keys with list(...). Accumulate at
most 50 keys per batch, process each batch before collecting the next, and
preserve the existing run_ids behavior.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@deep_agent/aegra/lifecycle.py`:
- Around line 361-364: Update the key-processing flow around the batch_size loop
to consume client.scan_iter(...) incrementally instead of materializing all keys
with list(...). Accumulate at most 50 keys per batch, process each batch before
collecting the next, and preserve the existing run_ids behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b740cdaf-7b2e-4f0e-a058-07f7d51e0605

📥 Commits

Reviewing files that changed from the base of the PR and between 46bde3d and 4ccccec.

📒 Files selected for processing (3)
  • deep_agent/aegra/lifecycle.py
  • scripts/inspect-db.sh
  • scripts/kind-pod-kill-test.sh
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • redhat-data-and-ai/template-mcp (manual)
  • redhat-data-and-ai/template-ui (manual)

@saharannaveen
saharannaveen force-pushed the feat/rhitaif-206-state-persistence branch from 9e23cae to 5e2d6d7 Compare July 28, 2026 13:45
@NP-compete NP-compete added the deep-agent PRs targeting the deep-agent branch label Aug 1, 2026
nsaharan added 11 commits August 3, 2026 16:33
…(RHITAIF-206)

Persist in-flight conversation state on SIGTERM so new pods can resume
interrupted runs from LangGraph checkpoints.

- Add lifecycle.py: Redis inflight tracking, Postgres run status updates,
  Fernet-encrypted token persistence, FOR UPDATE SKIP LOCKED resume
- Add shutdown step: persist_inflight_runs() marks active runs as interrupted
- Add startup step: resume_interrupted_runs() scans and reclaims orphaned runs
- Add graph.py import guard for lifecycle availability
- Add 4 lifecycle settings (feature-flagged, enabled by default)
- Add 22 unit tests + 6 integration tests (28 total, all passing)

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
…(RHITAIF-206)

Persist in-flight conversation state on SIGTERM so new pods can resume
interrupted runs from LangGraph checkpoints.

Components:
- lifecycle.py: Redis inflight tracking with status=active/interrupted,
  Fernet-encrypted SSO token persistence, FOR UPDATE SKIP LOCKED resume
  with lease-based claiming (claimed_by + lease_expires_at)
- shutdown.py: persist_inflight_runs() reads active_runs dict, marks
  runs interrupted in both Redis and Postgres
- startup.py: resume_interrupted_runs() scans for orphaned runs,
  claims with lease, loads checkpoint, rebuilds graph context
- graph.py: hooks register_inflight/deregister_inflight and attaches
  execution context to compiled graph for run boundary tracking
- settings.py: 4 feature-flagged lifecycle settings (default enabled)

Tests: 31 unit + 7 integration = 38 total, all passing

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
- Remove non-existent checkpoint_id column from runs query
- Select assistant_id and execution_params instead
- Update resume_fn signature to (run_id, thread_id)
- Fix integration tests to match new signatures

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
Signed-off-by: Naveen Saharan <nsaharan@redhat.com>

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
Direct ainvoke(None) cancels pending tool calls. Instead, use
POST /threads/{thread_id}/runs/wait with checkpoint config to
resume through the same path as a normal user request. This
ensures proper graph execution with checkpointer injection.

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
Per LangGraph docs, graph.ainvoke(None, config={thread_id}) resumes
from the latest checkpoint — nodes before the checkpoint are skipped,
nodes after re-execute including LLM calls and tool invocations.

Attach checkpointer and store from db_manager explicitly since we're
outside Aegra's normal request path.

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
Signed-off-by: Naveen Saharan <nsaharan@redhat.com>

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
…n-pod'

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
On shutdown, expire leases so the built-in LeaseReaper re-enqueues
interrupted runs through the standard Aegra worker path. No custom
graph building or resume code needed.

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
Signed-off-by: Naveen Saharan <nsaharan@redhat.com>

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
…sting

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
nsaharan added 10 commits August 3, 2026 16:33
- startup.py: add _init_aegra_db() to initialize Postgres checkpointer
  when running under raw uvicorn (production Containerfile)
- scripts/kind-pod-kill-test.sh: automated pod kill & recovery demo
  for Kind cluster testing

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
Signed-off-by: Naveen Saharan <nsaharan@redhat.com>

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
Signed-off-by: Naveen Saharan <nsaharan@redhat.com>

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
Kill only the agent (port 5002) instead of all services. Use
configurable PG_CONTAINER env var, clear MCP circuit breaker on
restart, and remove UI restart logic since it's managed separately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Naveen Saharan <nsaharan@redhat.com>

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
- Fix SQL parameterization bug: INTERVAL '%s seconds' → %s * INTERVAL '1 second'
- Replace Redis KEYS with SCAN for production safety
- Convert resume_interrupted_runs to async psycopg.AsyncConnection
- Add warning log when refresh token encryption is skipped
- Update test mocks for async connection pattern

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
Aegra's run_executor sets aegra:run:done:<id> even for interrupted runs.
On startup, clear done/counter/cache keys for interrupted runs being
re-enqueued so LeaseReaper can pick them up. Also wire up the
LIFECYCLE_RESUME_ON_STARTUP config flag and convert startup DB calls
to async psycopg.

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
Replace plain FastAPI REST server with FastMCP using Streamable HTTP
transport so the agent's MCP client can connect via the standard MCP
protocol at /mcp. Same 4 tools, same logic.

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
The graph factory tests pass MagicMock objects for model_name and
assistant_id, which fail Pydantic validation in build_execution_context.
Patch LIFECYCLE_PERSISTENCE_ENABLED to False since these tests don't
exercise lifecycle behavior.

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
- Batch Redis hgetall calls via pipeline in get_inflight_run_ids (N+1 fix)
- Batch lease expiry into single UPDATE ... WHERE run_id = ANY() query
- Add LIMIT to unbounded interrupted runs SELECT in startup.py
- Use psql variable binding in inspect-db.sh to prevent SQL injection
- Source DB credentials from env vars in kind-pod-kill-test.sh
- Use mktemp for secure temp file in pod-kill-and-recover.sh
- Add scan_iter and pipeline hgetall to _FakeRedis in integration tests
- Fix tautological test to call real _get_encryption_secret()
- Fix integration tests to mock AsyncConnection.connect (not sync)

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
- Batch Redis pipeline hgetall in groups of 50 to bound memory
- Fix worker pod history query to compare original vs recovery pod
- Fail explicitly when handler pod can't be matched instead of killing wrong replica

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
@saharannaveen
saharannaveen force-pushed the feat/rhitaif-206-state-persistence branch from 0ee1d83 to 5f64817 Compare August 3, 2026 14:12
@saharannaveen

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@deep_agent/aegra/graph.py`:
- Around line 385-407: The cached graph must not retain per-request lifecycle
state such as user identity or the encrypted refresh token. Update the flow
around build_execution_context and the _lifecycle_* setattr calls so this
context is supplied per request rather than stored on the shared compiled
object, or extend cache_key to include the user-specific identity before
_graph_cache lookup/storage; preserve shared graph reuse without cross-user
context leakage.

In `@deep_agent/aegra/lifecycle.py`:
- Around line 577-622: Change the resume batch flow around the SELECT/UPDATE
logic to claim all eligible rows and commit the transaction before invoking
resume_fn. Store the claimed run_id and thread_id values, then process them
afterward so resume_fn and its success/error updates do not run while the FOR
UPDATE locks are held; preserve per-run result recording, logging, and
deregister_inflight behavior.
- Around line 221-243: The register_inflight flow in lifecycle.py performs
synchronous psycopg.connect and UPDATE work on the async agent request path;
make this database write non-blocking by using an async PostgreSQL connection or
offloading the entire existing connection, cursor, commit, and update sequence
via asyncio.to_thread. Preserve the current started_by_pod update and debug
logging behavior while ensuring register_inflight does not block the event loop.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 16ad62de-44ff-447c-a103-baf6277fa831

📥 Commits

Reviewing files that changed from the base of the PR and between 4ccccec and 5f64817.

📒 Files selected for processing (11)
  • deep_agent/aegra/graph.py
  • deep_agent/aegra/lifecycle.py
  • deep_agent/aegra/shutdown.py
  • deep_agent/aegra/startup.py
  • deep_agent/src/infrastructure/subagents.py
  • deep_agent/src/settings.py
  • tests/integration/conftest.py
  • tests/integration/test_lifecycle_integration.py
  • tests/mocks/mock_mcp_server.py
  • tests/unit/aegra/test_graph.py
  • tests/unit/test_lifecycle.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • redhat-data-and-ai/template-mcp (manual)
  • redhat-data-and-ai/template-ui (manual)

Comment thread deep_agent/aegra/graph.py
Comment thread deep_agent/aegra/lifecycle.py Outdated
Comment thread deep_agent/aegra/lifecycle.py
@saharannaveen
saharannaveen force-pushed the feat/rhitaif-206-state-persistence branch from 5f64817 to dc57005 Compare August 3, 2026 20:14
Remove inspect-db.sh, pod-kill-and-recover.sh, and kind-pod-kill-test.sh
from the repo. These are local development/testing utilities.

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
When a pod is killed and the LeaseReaper re-enqueues a run, the stored
input_data and command cause LangGraph to re-process them on top of the
checkpoint state. This triggers PatchToolCallsMiddleware to mark
in-flight tool calls as cancelled, causing the orchestrator to
re-delegate and re-trigger HITL approval.

Fix: at startup, clear input_data and command from execution_params for
all recoverable runs before the LeaseReaper re-enqueues them. This makes
_resolve_input() return None so the graph resumes purely from checkpoint.

Also removes orphaned tools (queue_task, check_task_status,
get_pending_results) from PROMPT.md.

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
cryptography 49.0.0 has a Bleichenbacher-style timing oracle in
pkcs7_decrypt_* functions (CVSS 8.2, fixed in 50.0.0). Not exploitable
in template-agent — PKCS7 decrypt is never used; cryptography is a
transitive dep for TLS only.

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
The lifecycle:inflight:* Redis keys were never created in production.
register_inflight() was stashed via setattr on the compiled graph, but
Aegra's graph.copy() drops setattr attributes — the hook never fired.
All recovery operates through Postgres (LeaseReaper + checkpoints).

Removed: register_inflight, update_inflight_checkpoint,
_mark_inflight_interrupted, deregister_inflight, get_inflight_run_ids,
get_redis_client wrapper, REDIS_INFLIGHT_PREFIX, REDIS_INFLIGHT_TTL.

Simplified persist_inflight_runs (Postgres-only) and
resume_interrupted_runs (removed no-op deregister call).

Fixed startup.py importing get_redis_client from lifecycle instead of
redis module (was causing warnings on every restart).

Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
@saharannaveen saharannaveen changed the title feat: State persistence recovery feat: State persistence recovery fix Aug 10, 2026
Signed-off-by: Naveen Saharan <nsaharan@redhat.com>
@saharannaveen
saharannaveen force-pushed the feat/rhitaif-206-state-persistence branch from 2e8ae38 to a24a237 Compare August 10, 2026 08:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deep-agent PRs targeting the deep-agent branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: State persistence and recovery for agent runs across pod restarts

5 participants