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:
- 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.
- 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.
- 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:
- 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.
- 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.
- 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:
- 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_*).
- 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:
- Given
extractSessionIDFromLines in helpers.go, When F079 is complete, Then the function is removed entirely (no provider uses it).
- Given
codex- prefix detection in Codex provider, When F079 is complete, Then all prefix logic is removed.
- 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.
F079: Fix Session Resume for All CLI Providers
Scope
In Scope
session_idfrom stream-json output, use--resume <uuid>thread_idfrom JSON output, useresume <thread_id>sessionIDfrom JSON output (replace text pattern extraction)-cfallback for OpenCode when JSON session ID extraction failsextractSessionIDFromLineshelper (unused by all providers after fix)codex-prefix logic from Codex providerextractSessionIDmethods parsing provider-specific JSON (following Claude pattern)--output-format stream-jsonin GeminiExecuteConversation(like Claude already does)Out of Scope
Deferred
--list-sessionsintegration for precise session targeting--resume <uuid>with extracted ID is sufficientcodex resume <thread_id> --jsonproduces structured output on subsequent turnsresume --lastneeded as fallback for turn 3+--format jsonsession ID extraction edge case validationResearch Findings
CLI Output Formats
All 4 CLI providers emit session/thread IDs in their structured JSON output:
--output-format stream-jsonsession_id-r <uuid>--output-format stream-jsonsession_idtype: "init"--resume <uuid>exec --jsonthread_idtype: "thread.started"resume <thread_id>--format jsonsessionIDtype: "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 indexgemini --resume <uuid>— by session IDCodex (
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
extractSessionIDFromLinesinhelpers.go— searches for text patternSession: <id>that no provider emitscodex-prefix detection incodex_provider.go— entirely fabricated, never functionalExecuteConversationcallextractSessionIDFromLineswhich always fails →SessionIDalways reset to""→ resume never worksUser 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:
continue_fromlinking them, When the second step executes, Then Gemini CLI receives--resume <uuid>with the real session ID from the previous turn.SessionIDcontains the real UUID extracted from thetype: "init"stream-json line.--resumeflag is passed,--output-format stream-jsonis forced, andSessionIDis 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:
continue_fromlinking them, When the second step executes, Then Codex CLI receivesresume <thread_id>with the real thread ID from the previous turn.SessionIDcontains the realthread_idextracted from thetype: "thread.started"JSON line.codex-prefix detection logic exists in the codebase, When F079 is implemented, Then allcodex-prefix logic is removed.Independent Test: Run a 2-step Codex workflow with
continue_from. Verify the CLI invocation usesresume <thread_id>and nocodex-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:
--format json, When the ConversationState is persisted, ThenSessionIDcontains the realsessionIDextracted from the JSON output (e.g.,ses_*).-cflag is used as fallback to continue the last session.Independent Test: Mock OpenCode JSON output with
sessionIDfield. 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:
extractSessionIDFromLinesinhelpers.go, When F079 is complete, Then the function is removed entirely (no provider uses it).codex-prefix detection in Codex provider, When F079 is complete, Then all prefix logic is removed.extractSessionIDmethod parsing its own JSON format (following Claude's existing pattern).Independent Test:
grep -r extractSessionIDFromLinesreturns zero hits.grep -r "codex-"returns zero hits in provider code.make lintpasses.Edge Cases
continue_fromreferences a Gemini step? The real UUID is passed to--resume, targeting the exact session. No ambiguity.resume <id>is invoked with an empty ID? The provider must only passresume <thread_id>whenSessionID != "", not on first turn.-c(continue last session), then to stateless if-calso fails.Requirements
Functional Requirements
--output-format stream-jsoninExecuteConversationand extractsession_idfrom thetype: "init"JSON line.--resume <session_id>with the real UUID whenConversationState.SessionIDis non-empty.thread_idfrom thetype: "thread.started"JSON line inexec --jsonoutput.resume <thread_id>with the real thread ID whenConversationState.SessionIDis non-empty.codex-prefix detection and stripping logic MUST be removed from Codex provider.sessionIDfrom JSON output instead of usingextractSessionIDFromLinestext pattern.-cflag when JSON session ID extraction fails but a prior conversation step exists (viacontinue_from).extractSessionIDFromLinesMUST be removed fromhelpers.go(dead code for all providers).continue_fromcross-step resume MUST work with real session/thread IDs stored inConversationState.SessionID.-r <session_id>).Non-Functional Requirements
make lintandmake test-unitwith zero regressions.Success Criteria
codex-prefix,extractSessionIDFromLines) is removed from codebase.Key Entities
SessionID(stores real provider-specific IDs: UUIDs for Claude/Gemini/Codex,ses_*for OpenCode)Assumptions
--resume <uuid>resumes the session matching that UUID (confirmed via docs).resume <thread_id>resumes the thread matching that ID (confirmed via docs).-s <session_id>resumes the session matching that ID (already implemented).Metadata
Dependencies
Clarifications
--output-format stream-jsonemitssession_idintype: "init"line — sentinel"latest"replaced with real UUID extraction.exec --jsonemitsthread_idintype: "thread.started"line — sentinel"last"andcodex-prefix replaced with real thread ID extraction.--format jsonemitssessionIDintype: "step_start"line — text pattern extraction replaced with JSON parsing.extractSessionIDFromLinesconfirmed dead for all providers — to be removed entirely.codex resume <thread_id> --jsonproduce structured output? If not,resume --lastmay be needed as fallback for turn 3+.Notes
ConversationState.SessionIDvalues 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).extractSessionIDmethods (no shared interface) — output formats differ fundamentally between providers (different JSON field names, different line types).