fix: preserve durable session IDs across turns for Cursor/OpenCode - #34
Conversation
Cursor: The system/init handler was unconditionally overwriting resume_session_id with the transient per-run session_id. This caused the second turn's --resume flag to reference a stale/invalid ID instead of the durable chat ID from create-chat. Now only captures system/init session_id when no real chat ID exists yet. Also accept object-shaped resumeCursor values (already-decoded maps from normalize_resume_cursor) in addition to JSON strings. OpenCode: Always created a fresh session on startup instead of reusing a persisted sessionId from resumeCursor. Now extracts and verifies persisted session IDs, falling back to new session creation only when the persisted session is invalid. HarnessClientAdapter: sendTurn was stripping attachments and interactionMode when forwarding to the Elixir harness. Now passes all parameters consistently with how ProviderCommandReactor dispatches them. Co-Authored-By: Bastian Venegas Arevalo <r2d2@ranvier-technologies.com>
Original prompt from Bastian
|
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
📝 WalkthroughWalkthroughThis PR enables persisted session reuse by extracting and validating session IDs from Changes
Sequence DiagramsequenceDiagram
participant Client
participant Harness as Harness<br/>(Provider)
participant OpenCode as OpenCode<br/>Backend
participant SessionCreator as Session<br/>Creation
Client->>Harness: Connect with resumeCursor
Harness->>Harness: Normalize & extract sessionId from resumeCursor
rect rgba(100, 200, 100, 0.5)
Note over Harness,OpenCode: Reuse path (persisted ID present)
Harness->>OpenCode: GET /session/:id (verify)
OpenCode-->>Harness: 200 OK (valid)
Harness->>Harness: Set state.opencode_session_id / ready, persist binding
end
rect rgba(100, 100, 200, 0.5)
Note over Harness,SessionCreator: Fallback path (no/invalid ID)
Harness->>SessionCreator: POST /session (create)
SessionCreator-->>Harness: New session ID
Harness->>Harness: Store new session id, persist binding
end
Harness-->>Client: Session ready (reused or new)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…de verify, update bridge contract Cursor (CodeRabbit major): system/init fallback now also updates session_id and persists the binding via persist_binding/1 so the durable ID survives restarts. OpenCode (CodeRabbit major + Devin Review): verify_opencode_session now requires the returned 'id' to exactly match state.opencode_session_id. Any other 200-level response is treated as an error. Also cleaned up _reused binding. Bridge contract (Devin Review): Added 'attachments' to session.sendTurn params for doc consistency. Co-Authored-By: Bastian Venegas Arevalo <r2d2@ranvier-technologies.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/harness/lib/harness/dev/bridge_contract.ex`:
- Line 35: The bridge currently advertises an attachments param but OpenCode
doesn't support them end-to-end; update OpenCode handling to explicitly reject
attachments instead of silently dropping them: in
Harness.Providers.OpenCodeSession.handle_call/3 (and where send_prompt_async/3
is invoked) detect non-empty attachments on session.sendTurn requests and return
a clear error/validation response (or raise) indicating attachments are not
supported for OpenCode, and add tests; alternatively remove "attachments" from
the params list in bridge_contract.ex if you prefer to hide the capability
entirely.
In `@apps/harness/lib/harness/providers/opencode_session.ex`:
- Around line 172-190: The code reuses a persisted OpenCode session (via
verify_opencode_session and returning {:ok, state.opencode_session_id}) but
never repopulates state.messages before proceeding to the ready path, so
read_thread/2 still returns an empty conversation; update the branch that
handles {:ok, state.opencode_session_id} to call a hydration/readback routine
(e.g., invoke the existing read_thread/2 or a new fetch_messages_from_provider/1
helper) to populate state.messages (and any other session metadata) before
returning the {:ok, session_id} result so the subsequent ready path sees the
restored history; ensure this uses the same session ID returned by
create_opencode_session/1 when falling back.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 08442217-3cdc-49b0-9e45-5889fa6bd4a5
📒 Files selected for processing (3)
apps/harness/lib/harness/dev/bridge_contract.exapps/harness/lib/harness/providers/cursor_session.exapps/harness/lib/harness/providers/opencode_session.ex
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/harness/lib/harness/providers/cursor_session.ex
| %{ | ||
| event: "session.sendTurn", | ||
| params: ~w(threadId input model effort interactionMode modelOptions), | ||
| params: ~w(threadId input model effort interactionMode modelOptions attachments), |
There was a problem hiding this comment.
Don't advertise attachment support until OpenCode can consume it.
session.sendTurn now accepts attachments, but Harness.Providers.OpenCodeSession.handle_call/3 still strips the payload down to text and send_prompt_async/3 only posts a single text part (apps/harness/lib/harness/providers/opencode_session.ex:350-394 and apps/harness/lib/harness/providers/opencode_session.ex:1288-1307). For OpenCode, turns with attachments will still succeed while silently losing that context. Either plumb attachments through end-to-end or reject them explicitly for unsupported providers.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/harness/lib/harness/dev/bridge_contract.ex` at line 35, The bridge
currently advertises an attachments param but OpenCode doesn't support them
end-to-end; update OpenCode handling to explicitly reject attachments instead of
silently dropping them: in Harness.Providers.OpenCodeSession.handle_call/3 (and
where send_prompt_async/3 is invoked) detect non-empty attachments on
session.sendTurn requests and return a clear error/validation response (or
raise) indicating attachments are not supported for OpenCode, and add tests;
alternatively remove "attachments" from the params list in bridge_contract.ex if
you prefer to hide the capability entirely.
| # Reuse a persisted session when available; otherwise create a new one. | ||
| # This preserves conversation history across session restarts/resumes. | ||
| {session_result, _reused} = | ||
| if state.opencode_session_id do | ||
| # Verify the persisted session still exists on the server | ||
| case verify_opencode_session(state) do | ||
| :ok -> | ||
| Logger.info("Reusing persisted OpenCode session #{state.opencode_session_id} for thread #{state.thread_id}") | ||
| {{:ok, state.opencode_session_id}, true} | ||
|
|
||
| {:error, reason} -> | ||
| Logger.info("Persisted OpenCode session invalid (#{inspect(reason)}), creating new session for thread #{state.thread_id}") | ||
| {create_opencode_session(state), false} | ||
| end | ||
| else | ||
| {create_opencode_session(state), false} | ||
| end | ||
|
|
||
| case session_result do |
There was a problem hiding this comment.
Reused sessions still come back with an empty harness thread.
This branch verifies the remote session and immediately moves to the ready path, but it never repopulates state.messages first. Since read_thread/2 is built entirely from that in-memory list, a restarted harness will still report no prior turns even though the provider-side session was reused. A hydration/readback step is still needed before emitting ready if we want full continuity.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/harness/lib/harness/providers/opencode_session.ex` around lines 172 -
190, The code reuses a persisted OpenCode session (via verify_opencode_session
and returning {:ok, state.opencode_session_id}) but never repopulates
state.messages before proceeding to the ready path, so read_thread/2 still
returns an empty conversation; update the branch that handles {:ok,
state.opencode_session_id} to call a hydration/readback routine (e.g., invoke
the existing read_thread/2 or a new fetch_messages_from_provider/1 helper) to
populate state.messages (and any other session metadata) before returning the
{:ok, session_id} result so the subsequent ready path sees the restored history;
ensure this uses the same session ID returned by create_opencode_session/1 when
falling back.
What Changed
Three fixes for harness-backed providers (Cursor, OpenCode) failing on follow-up questions:
1. Cursor: stop clobbering the durable chat ID (primary fix)
handle_stream_messageforsystem/initwas unconditionally overwritingresume_session_idwith the transient per-run session ID. On the second turn,--resumewould reference this stale ID instead of the durable chat ID fromcreate-chat. Now only capturessystem/initsession_id whenhas_real_chat_idis false.Also added map-shaped
resumeCursorhandling toextract_resume_session_id(D4 parity).2. OpenCode: reuse persisted session on restart/resume
init/1always created a fresh OpenCode session, discarding any persistedsessionIdfromresumeCursor. Now extracts the persisted ID, verifies it viaGET /session/:id, and falls back to a new session only if verification fails.3. HarnessClientAdapter: forward missing sendTurn params
sendTurnwas only passinginputandmodelto the harness —attachmentsandinteractionModewere silently dropped. Now forwards them consistently with howProviderCommandReactor.sendTurnForThreaddispatches.Why
Claude and Codex (direct adapters) handle multi-turn correctly. Cursor and OpenCode (harness path) fail on the second question because:
--resumeflag gets a stale session ID after turn 1attachments/interactionModecould cause subtle context lossChecklist
Review focus areas
system/initguard logic (cursor_session.ex:563-568): Verify that the transientsystem/initsession_id is indeed never the correct resume ID when a durable chat ID already exists fromcreate-chat.verify_opencode_session: The{:ok, _} -> :okcatch-all is permissive — a 200 response with unexpected shape will pass verification. Confirm this is safe given OpenCode's API contract._ = reused(opencode_session.ex:193): Suppresses unused variable warning. Harmless but worth noting — could be removed if the variable isn't needed for future logging.attachments/interactionMode: The harness adapter now forwards these, but confirm the Elixir session handlers actually use them (or at least tolerate the extra keys inparams).Link to Devin session: https://app.devin.ai/sessions/7c753c05392f4faeab829e3b269299e8
Requested by: @ranvier2d2
Summary by CodeRabbit
Bug Fixes
New Features