Skip to content

F079: Fix Session Resume for Gemini and Codex (Breaking Change) #302

@pocky

Description

@pocky

F079: Fix Session Resume for All CLI Providers

Scope

In Scope

  • Fix Gemini session resume: extract real session_id from stream-json output, use --resume <uuid>
  • Fix Codex session resume: extract real thread_id from JSON output, use resume <thread_id>
  • Fix OpenCode session resume: extract real sessionID from JSON output (replace text pattern extraction)
  • Add -c fallback for OpenCode when JSON session ID extraction fails
  • Remove dead extractSessionIDFromLines helper (unused by all providers after fix)
  • Remove fabricated codex- prefix logic from Codex provider
  • Add per-provider extractSessionID methods parsing provider-specific JSON (following Claude pattern)
  • Force --output-format stream-json in Gemini ExecuteConversation (like Claude already does)
  • Update all affected tests
  • Document breaking changes in CHANGELOG.md

Out of Scope

  • Claude provider changes (already working correctly with JSON extraction)
  • OpenAI Compatible provider changes (HTTP-based, no CLI resume)
  • New session management features (session listing, session picker)
  • Multi-session orchestration or session routing

Deferred

Item Rationale Follow-up
Gemini --list-sessions integration for precise session targeting --resume <uuid> with extracted ID is sufficient future
Verify codex resume <thread_id> --json produces structured output on subsequent turns If not, resume --last needed as fallback for turn 3+ future
OpenCode --format json session ID extraction edge case validation Current JSON parsing should work reliably future

Research Findings

CLI Output Formats

All 4 CLI providers emit session/thread IDs in their structured JSON output:

Provider Forced format JSON field JSON line type Resume flag
Claude --output-format stream-json session_id first object -r <uuid>
Gemini --output-format stream-json session_id type: "init" --resume <uuid>
Codex exec --json thread_id type: "thread.started" resume <thread_id>
OpenCode --format json sessionID type: "step_start" -s <id>

Evidence

Gemini (gemini -p "Say hello" --output-format stream-json):

{"type":"init","timestamp":"2026-04-07T21:55:36.994Z","session_id":"031da63a-73be-42f5-ae0d-890aae0b6323","model":"auto-gemini-3"}

Gemini resume flags (from docs):

  • gemini --resume — latest session (no arg)
  • gemini --resume <index> — by index
  • gemini --resume <uuid> — by session ID

Codex (codex exec --json "Return OK."):

{"type":"thread.started","thread_id":"019bd456-d3d4-70c3-90de-51d31a6c8571"}

OpenCode (opencode run "..." --format json):

{"type":"step_start","timestamp":1775599542766,"sessionID":"ses_296052f0bffeFudXE4xOn0vSEJ",...}

Dead Code Identified

  • extractSessionIDFromLines in helpers.go — searches for text pattern Session: <id> that no provider emits
  • codex- prefix detection in codex_provider.go — entirely fabricated, never functional
  • Both Gemini and Codex ExecuteConversation call extractSessionIDFromLines which always fails → SessionID always reset to "" → resume never works

User Stories

US1: Gemini Session Resume Works (P1 - Must Have)

As a workflow author using Gemini as a conversation provider,
I want multi-turn conversations to resume correctly across steps,
So that Gemini retains context from previous turns instead of silently falling back to stateless mode.

Acceptance Scenarios:

  1. Given a workflow with 2+ Gemini conversation steps and continue_from linking them, When the second step executes, Then Gemini CLI receives --resume <uuid> with the real session ID from the previous turn.
  2. Given a Gemini conversation step completes successfully, When the ConversationState is persisted, Then SessionID contains the real UUID extracted from the type: "init" stream-json line.
  3. Given a first Gemini conversation step (no prior session), When it executes, Then no --resume flag is passed, --output-format stream-json is forced, and SessionID is extracted from output after completion.

Independent Test: Run a 2-step Gemini workflow with continue_from. Verify the second step's CLI args include --resume <uuid> matching the first step's extracted session ID.

US2: Codex Session Resume Works (P1 - Must Have)

As a workflow author using Codex as a conversation provider,
I want multi-turn conversations to resume correctly across steps,
So that Codex retains context from previous turns instead of silently falling back to stateless mode.

Acceptance Scenarios:

  1. Given a workflow with 2+ Codex conversation steps and continue_from linking them, When the second step executes, Then Codex CLI receives resume <thread_id> with the real thread ID from the previous turn.
  2. Given a Codex conversation step completes, When the ConversationState is persisted, Then SessionID contains the real thread_id extracted from the type: "thread.started" JSON line.
  3. Given the codex- prefix detection logic exists in the codebase, When F079 is implemented, Then all codex- prefix logic is removed.

Independent Test: Run a 2-step Codex workflow with continue_from. Verify the CLI invocation uses resume <thread_id> and no codex- prefix appears in state files.

US3: OpenCode Resume Uses JSON Extraction (P2 - Should Have)

As a workflow author using OpenCode as a conversation provider,
I want session resume to use the real session ID extracted from JSON output,
So that conversations resume precisely instead of relying on fragile text pattern matching.

Acceptance Scenarios:

  1. Given an OpenCode conversation step completes with --format json, When the ConversationState is persisted, Then SessionID contains the real sessionID extracted from the JSON output (e.g., ses_*).
  2. Given an OpenCode conversation step where JSON session ID extraction fails, When the next step resumes, Then -c flag is used as fallback to continue the last session.

Independent Test: Mock OpenCode JSON output with sessionID field. Verify extraction succeeds and -s <id> is used on resume.

US4: Dead Code Removal (P2 - Should Have)

As a maintainer of the AWF CLI,
I want fabricated session extraction logic removed from all providers,
So that the codebase accurately reflects what the CLIs actually support.

Acceptance Scenarios:

  1. Given extractSessionIDFromLines in helpers.go, When F079 is complete, Then the function is removed entirely (no provider uses it).
  2. Given codex- prefix detection in Codex provider, When F079 is complete, Then all prefix logic is removed.
  3. Given each provider needs session ID extraction, When F079 is complete, Then each has a private extractSessionID method parsing its own JSON format (following Claude's existing pattern).

Independent Test: grep -r extractSessionIDFromLines returns zero hits. grep -r "codex-" returns zero hits in provider code. make lint passes.

Edge Cases

  • What happens when a Gemini workflow runs 2 parallel Gemini conversation steps? Both extract different session IDs from their own output — no conflict (unlike the sentinel approach where both would resume the same session).
  • What happens when continue_from references a Gemini step? The real UUID is passed to --resume, targeting the exact session. No ambiguity.
  • What happens when Codex has no prior session and resume <id> is invoked with an empty ID? The provider must only pass resume <thread_id> when SessionID != "", not on first turn.
  • What happens when OpenCode JSON parsing fails? Fall back to -c (continue last session), then to stateless if -c also fails.

Requirements

Functional Requirements

  • FR-001: Gemini provider MUST force --output-format stream-json in ExecuteConversation and extract session_id from the type: "init" JSON line.
  • FR-002: Gemini provider MUST use --resume <session_id> with the real UUID when ConversationState.SessionID is non-empty.
  • FR-003: Codex provider MUST extract thread_id from the type: "thread.started" JSON line in exec --json output.
  • FR-004: Codex provider MUST use resume <thread_id> with the real thread ID when ConversationState.SessionID is non-empty.
  • FR-005: All codex- prefix detection and stripping logic MUST be removed from Codex provider.
  • FR-006: OpenCode provider MUST extract sessionID from JSON output instead of using extractSessionIDFromLines text pattern.
  • FR-007: OpenCode provider MUST fall back to -c flag when JSON session ID extraction fails but a prior conversation step exists (via continue_from).
  • FR-008: extractSessionIDFromLines MUST be removed from helpers.go (dead code for all providers).
  • FR-009: continue_from cross-step resume MUST work with real session/thread IDs stored in ConversationState.SessionID.
  • FR-010: Claude provider resume behavior MUST remain unchanged (-r <session_id>).

Non-Functional Requirements

  • NFR-001: No new dependencies introduced — changes are surgical to existing provider files.
  • NFR-002: All changes must pass make lint and make test-unit with zero regressions.

Success Criteria

  • SC-001: Gemini multi-turn workflows resume context via real session UUID in 100% of sequential conversation steps.
  • SC-002: Codex multi-turn workflows resume context via real thread ID in 100% of sequential conversation steps.
  • SC-003: OpenCode session resume uses real session ID from JSON extraction.
  • SC-004: Zero regressions in Claude session resume behavior.
  • SC-005: All fabricated session ID logic (codex- prefix, extractSessionIDFromLines) is removed from codebase.

Key Entities

Entity Description Key Attributes
ConversationState Tracks session continuity across conversation turns SessionID (stores real provider-specific IDs: UUIDs for Claude/Gemini/Codex, ses_* for OpenCode)

Assumptions

  • Gemini CLI --resume <uuid> resumes the session matching that UUID (confirmed via docs).
  • Codex CLI resume <thread_id> resumes the thread matching that ID (confirmed via docs).
  • OpenCode CLI -s <session_id> resumes the session matching that ID (already implemented).
  • All providers emit session/thread IDs in their JSON/stream-json output (confirmed via testing).
  • F078 CLI flag corrections are already in place before F079 implementation begins.

Metadata

  • Status: backlog
  • Version: v0.7.0
  • Priority: high
  • Estimation: M

Dependencies

  • Blocked by: F078
  • Unblocks: none

Clarifications

  • 2026-04-07: Gemini CLI --output-format stream-json emits session_id in type: "init" line — sentinel "latest" replaced with real UUID extraction.
  • 2026-04-07: Codex CLI exec --json emits thread_id in type: "thread.started" line — sentinel "last" and codex- prefix replaced with real thread ID extraction.
  • 2026-04-07: OpenCode CLI --format json emits sessionID in type: "step_start" line — text pattern extraction replaced with JSON parsing.
  • 2026-04-07: extractSessionIDFromLines confirmed dead for all providers — to be removed entirely.
  • Open: Does codex resume <thread_id> --json produce structured output? If not, resume --last may be needed as fallback for turn 3+.

Notes

  • This is a breaking change: stored ConversationState.SessionID values from prior runs become invalid for Gemini and Codex. Old values will not match any real session, causing resume to skip (safe fallback to stateless).
  • The implementation follows Claude's proven pattern: force structured output → extract real ID → resume by ID. This is now uniform across all 4 CLI providers.
  • Per-provider private extractSessionID methods (no shared interface) — output formats differ fundamentally between providers (different JSON field names, different line types).
  • Parallel conversation steps within a single workflow now work correctly for Gemini (each step gets its own UUID), unlike the sentinel approach where both would resume the same session.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions