Skip to content

fix(runner): preserve chat context when repo added mid-session#894

Merged
Gkrumbach07 merged 3 commits intomainfrom
fix/context-loss-on-repo-add-52263
Mar 13, 2026
Merged

fix(runner): preserve chat context when repo added mid-session#894
Gkrumbach07 merged 3 commits intomainfrom
fix/context-loss-on-repo-add-52263

Conversation

@Gkrumbach07
Copy link
Contributor

@Gkrumbach07 Gkrumbach07 commented Mar 12, 2026

Summary

  • Pass saved CLI session ID to adapter rebuild via resume_from parameter
  • Preserves session IDs in the bridge across mark_dirty() adapter teardowns
  • Adds get_all_session_ids() public method to SessionManager to avoid private attribute access

Test plan

  • Unit tests pass (452 passed)
  • Lints pass (ruff check + ruff format)
  • Manual test: add repo mid-session, verify chat history preserved

Jira: RHOAIENG-52263

🤖 Generated with Claude Code

@coderabbitai
Copy link

coderabbitai bot commented Mar 12, 2026

Walkthrough

Adds session-resume support for the Claude adapter: adapter.build_options() gains an optional resume_from parameter; bridge persists per-thread session IDs across adapter rebuilds and pod restarts; session manager/worker expose get_all_session_ids() and persist session IDs to disk.

Changes

Cohort / File(s) Summary
Adapter Resume Parameter
components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py
Added optional resume_from: Optional[str] to ClaudeAgentAdapter.build_options() and propagate it into merged options as resume when provided; updated docstring.
Session Persistence in Bridge
components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py
Added _saved_session_ids: dict[str, str]; run() looks up saved session ID per thread and passes it as resume_from when building options; mark_dirty() captures current session IDs into _saved_session_ids; _setup_platform() initializes SessionManager with a state_dir.
Session Manager / Worker Persistence & API
components/runners/ambient-runner/ambient_runner/bridges/claude/session.py
Added get_all_session_ids(self) -> dict[str, str] to SessionWorker and SessionManager; SessionManager.__init__ now accepts state_dir: str = ""; introduced disk persistence/load of session IDs (store/restore across restarts); minor import/format changes.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client/Runner
    participant Bridge as Claude Bridge
    participant SessionMgr as SessionManager
    participant Adapter as ClaudeAgentAdapter

    Client->>Bridge: mark_dirty() (prepare rebuild)
    Bridge->>SessionMgr: get_all_session_ids()
    SessionMgr-->>Bridge: {thread_id: session_id, ...}
    Bridge->>Bridge: store in _saved_session_ids

    Client->>Bridge: run(thread_id)
    Bridge->>Bridge: lookup saved_session_id = _saved_session_ids[thread_id]
    Bridge->>Adapter: build_options(resume_from=saved_session_id)
    Adapter-->>Bridge: ClaudeAgentOptions (includes resume)
    Bridge->>SessionMgr: persist session_id (post-turn)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: preserving chat context when a repository is added during an active session, which aligns with the PR's core objective of persisting session IDs across adapter rebuilds.
Description check ✅ Passed The description is directly related to the changeset, outlining the key changes (resume_from parameter, session ID preservation, get_all_session_ids method) and linking to the Jira issue, though manual testing is still pending.
Docstring Coverage ✅ Passed Docstring coverage is 81.25% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/context-loss-on-repo-add-52263
📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

When a repo is added mid-session, mark_dirty() destroys the adapter and
session manager. Previously the saved CLI session ID was lost, causing
Claude Code to start a fresh session without chat history.

Now the bridge preserves session IDs across adapter rebuilds and passes
them to build_options() via the new resume_from parameter, which sets
the resume option so the CLI resumes the existing conversation.

Also adds get_all_session_ids() to SessionManager to avoid accessing
private _workers attribute from the bridge.

Fixes: RHOAIENG-52263

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ambient-code ambient-code bot force-pushed the fix/context-loss-on-repo-add-52263 branch from 4de80aa to ec8a878 Compare March 12, 2026 20:12
@ambient-code
Copy link
Contributor

ambient-code bot commented Mar 12, 2026

🤖 PR Fixer Report

✅ Rebase Complete

  • Rebased onto: main (373db39)
  • Resolved conflicts: components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py
    • Removed _halted_by_thread state (removed from main)
    • Preserved _saved_session_ids state (this PR's addition)

📊 Reviewer Feedback

  • 1 comment from @coderabbitai (informational summary, no action needed)
  • 0 inline review comments to address

✅ Quality Checks

  • Lints: Passed (ruff format + ruff check via pre-commit)
  • CI Status: 27 checks passing
  • Code review: Ran /simplify - no critical issues found

🚀 Changes Pushed

  • Commit: ec8a878 (rebased from 4de80aa)
  • Files changed: 3 files (+30/-2 lines)
  • Force pushed to fix/context-loss-on-repo-add-52263

📝 Summary

Successfully rebased PR to resolve merge conflicts with main. The conflict was due to _halted_by_thread being removed from the bridge in main while this PR added both _halted_by_thread and _saved_session_ids. Resolved by keeping only the _saved_session_ids addition that this PR needs for preserving session context across adapter rebuilds.

All lints pass, CI is green, ready for merge.

Copy link

@coderabbitai coderabbitai bot left a comment

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 (1)
components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py (1)

458-468: ⚠️ Potential issue | 🟠 Major

Prevent caller forwarded_props from overriding the bridge-managed resume target.

resume_from is written here, but apply_forwarded_props() runs afterward and can replace it with forwarded_props["resume"]. That lets a stale or cross-thread client value defeat the preserved session ID, which can either drop context or attach the run to the wrong Claude session.

Suggested change
-        # Resume from a previous CLI session (preserves chat context)
-        if resume_from:
-            merged_kwargs["resume"] = resume_from
-
         logger.debug(f"Merged kwargs after pop: {merged_kwargs}")

         # Apply forwarded_props as per-run overrides (before adding dynamic tools)
         if input_data and input_data.forwarded_props:
             merged_kwargs = apply_forwarded_props(
                 input_data.forwarded_props, merged_kwargs, ALLOWED_FORWARDED_PROPS
             )
+
+        # Bridge-managed resume must win over caller overrides.
+        if resume_from:
+            merged_kwargs["resume"] = resume_from
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py` around lines
458 - 468, The forwarded_props can override the bridge-managed resume target
because apply_forwarded_props(input_data.forwarded_props, ...) runs after
resume_from is set into merged_kwargs; to fix, filter out the "resume" key from
input_data.forwarded_props (or a shallow copy) before calling
apply_forwarded_props so forwarded_props cannot replace merged_kwargs["resume"],
or alternatively re-assign merged_kwargs["resume"] = resume_from immediately
after apply_forwarded_props; update the code paths around merged_kwargs,
resume_from and apply_forwarded_props (and consider ALLOWED_FORWARDED_PROPS) to
ensure "resume" is never allowed to be overridden by forwarded_props.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py`:
- Around line 104-107: The bridge currently pops the saved resume token
(saved_session_id) before the replacement worker is confirmed healthy; instead,
pass the token into Adapter.build_options by peeking (e.g., using get or
indexing into _saved_session_ids) so you do not remove it, and only remove (pop)
the saved token once the new worker has emitted its init/SystemMessage and
SessionWorker.session_id has been populated (i.e., move the pop into the worker
init/handler in
components/runners/ambient-runner/ambient_runner/bridges/claude/session.py where
SessionWorker.session_id is set); keep the resume_from field populated on retry
paths until successful init.
- Around line 175-177: mark_dirty() can race active turns because
SessionWorker.session_id is set lazily, so when /repos/add calls mark_dirty() it
may snapshot no ID and later teardown the worker losing the only resume target;
update the teardown logic in the bridge to either (a) defer destroying workers
until per-thread locks/drainers are released (wait for the worker's active-turn
lock or completion future) before removing it from the session manager, or (b)
persist the final session_id during the worker shutdown path by reading
SessionWorker.session_id and updating self._saved_session_ids (instead of
relying only on self._session_manager.get_all_session_ids()); implement one of
these in the code paths that call mark_dirty() and in the worker shutdown
routine so the final session ID is captured reliably.

---

Outside diff comments:
In `@components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py`:
- Around line 458-468: The forwarded_props can override the bridge-managed
resume target because apply_forwarded_props(input_data.forwarded_props, ...)
runs after resume_from is set into merged_kwargs; to fix, filter out the
"resume" key from input_data.forwarded_props (or a shallow copy) before calling
apply_forwarded_props so forwarded_props cannot replace merged_kwargs["resume"],
or alternatively re-assign merged_kwargs["resume"] = resume_from immediately
after apply_forwarded_props; update the code paths around merged_kwargs,
resume_from and apply_forwarded_props (and consider ALLOWED_FORWARDED_PROPS) to
ensure "resume" is never allowed to be overridden by forwarded_props.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4e73c84f-0b37-49d2-9383-5ebd7ac4b025

📥 Commits

Reviewing files that changed from the base of the PR and between 4de80aa and ec8a878.

📒 Files selected for processing (3)
  • components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py
  • components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py
  • components/runners/ambient-runner/ambient_runner/bridges/claude/session.py

Comment on lines +104 to +107
saved_session_id = self._saved_session_ids.pop(thread_id, None)
sdk_options = self._adapter.build_options(
input_data, thread_id=thread_id, resume_from=saved_session_id
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Do not consume the saved resume token before the replacement worker is healthy.

Line 104 removes the only bridge-level session ID before the new worker has connected and emitted a fresh init message. If worker startup fails in that gap, the retry path no longer has any resume_from value and the next run restarts a brand-new conversation. SessionWorker.session_id is only populated later from the init SystemMessage in components/runners/ambient-runner/ambient_runner/bridges/claude/session.py Lines 133-140.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py`
around lines 104 - 107, The bridge currently pops the saved resume token
(saved_session_id) before the replacement worker is confirmed healthy; instead,
pass the token into Adapter.build_options by peeking (e.g., using get or
indexing into _saved_session_ids) so you do not remove it, and only remove (pop)
the saved token once the new worker has emitted its init/SystemMessage and
SessionWorker.session_id has been populated (i.e., move the pop into the worker
init/handler in
components/runners/ambient-runner/ambient_runner/bridges/claude/session.py where
SessionWorker.session_id is set); keep the resume_from field populated on retry
paths until successful init.

Gkrumbach07 and others added 2 commits March 12, 2026 15:55
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…od restarts

SessionManager now saves session IDs to claude_session_ids.json on the
PVC so they survive pod restarts. On resume, restored IDs are passed
as resume_from to build_options, enabling the CLI to continue the
previous conversation.

Fixes: RHOAIENG-52263

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py (1)

457-468: 🧹 Nitpick | 🔵 Trivial

forwarded_props may override the persisted resume_from value.

The resume key is set at line 460, but apply_forwarded_props runs afterward (lines 465-468). Per the docstring (lines 131-133), resume is in the allowed forwarded props whitelist. If a client passes forwarded_props={"resume": "..."}, it will overwrite the resume_from session ID intended for context preservation.

If override is intentional (explicit user control), this is fine. If resume_from should take precedence, move lines 458-460 after apply_forwarded_props.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py` around lines
457 - 468, The current logic applies forwarded_props after setting
merged_kwargs["resume"] (using resume_from), which lets forwarded_props override
the persisted resume; to ensure the explicit resume_from takes precedence, call
apply_forwarded_props (with input_data.forwarded_props, merged_kwargs,
ALLOWED_FORWARDED_PROPS) first and then, if resume_from is present, set
merged_kwargs["resume"] = resume_from (i.e., move the resume assignment to after
apply_forwarded_props), referring to merged_kwargs, resume_from,
apply_forwarded_props, and ALLOWED_FORWARDED_PROPS to locate the change.
♻️ Duplicate comments (1)
components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py (1)

104-110: ⚠️ Potential issue | 🟠 Major

Session ID is consumed before worker confirms successful resume.

Line 105 uses pop() to remove the saved session ID before the worker has connected and confirmed the session was resumed. If get_or_create() fails or the CLI rejects the resume token, the ID is already lost and cannot be retried.

Consider using get() to peek the value, then only removing it after confirming the worker's session_id is populated (i.e., the CLI accepted the resume):

🛠️ Proposed fix
         thread_id = input_data.thread_id or self._context.session_id
         api_key = os.getenv("ANTHROPIC_API_KEY", "")
         saved_session_id = (
-            self._saved_session_ids.pop(thread_id, None)
+            self._saved_session_ids.get(thread_id)
             or self._session_manager.get_session_id(thread_id)
         )
         sdk_options = self._adapter.build_options(
             input_data, thread_id=thread_id, resume_from=saved_session_id
         )
         worker = await self._session_manager.get_or_create(
             thread_id, sdk_options, api_key
         )
+
+        # Only clear saved ID after worker confirms session resumed
+        if worker.session_id and thread_id in self._saved_session_ids:
+            self._saved_session_ids.pop(thread_id, None)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py`
around lines 104 - 110, The code currently uses
self._saved_session_ids.pop(thread_id, None) to read the saved session id before
attempting resume, which loses the token if resume fails; change this to peek
with self._saved_session_ids.get(thread_id) (or call
_session_manager.get_session_id(thread_id) only) and pass that value into
adapter.build_options/resume flow (see build_options and saved_session_id
usage), then only remove the saved id (pop) after the worker confirms a
successful resume—i.e., after the worker object has a populated session_id or
get_or_create() returns success/CLI accepted the resume—so failures keep the
token for retry.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py`:
- Around line 132-136: The bridge is directly manipulating SessionManager
internals (_session_ids and _persist_session_ids), breaking encapsulation;
modify SessionManager (class SessionManager) to add a public method like
update_session_id(thread_id, session_id) that sets the mapping and calls
persistence, then replace direct accesses in bridge.py (where worker.session_id
and thread_id are used) to call session_manager.update_session_id(thread_id,
worker.session_id) instead of touching _session_ids or _persist_session_ids
directly.

---

Outside diff comments:
In `@components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py`:
- Around line 457-468: The current logic applies forwarded_props after setting
merged_kwargs["resume"] (using resume_from), which lets forwarded_props override
the persisted resume; to ensure the explicit resume_from takes precedence, call
apply_forwarded_props (with input_data.forwarded_props, merged_kwargs,
ALLOWED_FORWARDED_PROPS) first and then, if resume_from is present, set
merged_kwargs["resume"] = resume_from (i.e., move the resume assignment to after
apply_forwarded_props), referring to merged_kwargs, resume_from,
apply_forwarded_props, and ALLOWED_FORWARDED_PROPS to locate the change.

---

Duplicate comments:
In `@components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py`:
- Around line 104-110: The code currently uses
self._saved_session_ids.pop(thread_id, None) to read the saved session id before
attempting resume, which loses the token if resume fails; change this to peek
with self._saved_session_ids.get(thread_id) (or call
_session_manager.get_session_id(thread_id) only) and pass that value into
adapter.build_options/resume flow (see build_options and saved_session_id
usage), then only remove the saved id (pop) after the worker confirms a
successful resume—i.e., after the worker object has a populated session_id or
get_or_create() returns success/CLI accepted the resume—so failures keep the
token for retry.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8dce55d5-851e-4a46-ba1b-a1c8de254c11

📥 Commits

Reviewing files that changed from the base of the PR and between ec8a878 and 1e1c14f.

📒 Files selected for processing (3)
  • components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py
  • components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py
  • components/runners/ambient-runner/ambient_runner/bridges/claude/session.py

Comment on lines +132 to +136
# Persist session ID after turn completes (for --resume on pod restart)
if worker.session_id:
self._session_manager._session_ids[thread_id] = worker.session_id
self._session_manager._persist_session_ids()

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Direct access to private _session_ids and _persist_session_ids() breaks encapsulation.

The bridge manipulates SessionManager internals directly. Consider adding a public method like update_session_id(thread_id, session_id) that handles both assignment and persistence:

♻️ Suggested approach

In session.py, add:

def update_session_id(self, thread_id: str, session_id: str) -> None:
    """Update and persist session ID for a thread."""
    self._session_ids[thread_id] = session_id
    self._persist_session_ids()

Then in bridge.py:

-            if worker.session_id:
-                self._session_manager._session_ids[thread_id] = worker.session_id
-                self._session_manager._persist_session_ids()
+            if worker.session_id:
+                self._session_manager.update_session_id(thread_id, worker.session_id)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py`
around lines 132 - 136, The bridge is directly manipulating SessionManager
internals (_session_ids and _persist_session_ids), breaking encapsulation;
modify SessionManager (class SessionManager) to add a public method like
update_session_id(thread_id, session_id) that sets the mapping and calls
persistence, then replace direct accesses in bridge.py (where worker.session_id
and thread_id are used) to call session_manager.update_session_id(thread_id,
worker.session_id) instead of touching _session_ids or _persist_session_ids
directly.

@Gkrumbach07 Gkrumbach07 merged commit 2f0b13d into main Mar 13, 2026
21 of 22 checks passed
@Gkrumbach07 Gkrumbach07 deleted the fix/context-loss-on-repo-add-52263 branch March 13, 2026 04:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant