Skip to content

Recover Sessions with StatefulSets - #1455

Merged
gjkim42 merged 1 commit into
mainfrom
session-recovery
Jul 13, 2026
Merged

gjkim42 merged 1 commit into
mainfrom
session-recovery

Conversation

@gjkim42

@gjkim42 gjkim42 commented Jul 11, 2026 •

Copy link
Copy Markdown
Collaborator

What type of PR is this?

/kind feature

What this PR does / why we need it:

Makes a Session a recoverable remote-control surface for one provider conversation.

  • backs each Session with a one-replica StatefulSet and a headless governing Service
  • adds optional Session.spec.volumeClaimTemplate; configured Sessions persist the workspace and runtime journal in a StatefulSet volume claim, while omitted storage uses emptyDir
  • restores persisted conversation events after Pod replacement and marks unfinished work as interrupted without replaying its prompt
  • reconnects terminal and web clients to replacement Pods while preserving accepted-request semantics
  • covers StatefulSet ownership, optional storage, journaling, request delivery, and recovery behavior in unit, integration, and end-to-end tests

Which issue(s) this PR is related to:

N/A

Special notes for your reviewer:

The headless Service is only the StatefulSet governing Service. Terminal and web clients continue to use the shared Session control path rather than connecting through a per-Session Service.

Pod replacement preserves workspace and conversation state only when spec.volumeClaimTemplate is configured. A Session that omits it receives a fresh emptyDir after replacement.

Validated locally with make update, make build, make test, make verify, and make test-integration (123 controller/CLI integration specs and 12 install-integration specs). The full e2e suite runs in CI.

Does this PR introduce a user-facing change?

Sessions now run as StatefulSets and can preserve their workspace and conversation events across Pod replacement when configured with `spec.volumeClaimTemplate`; terminal and web clients reconnect automatically.

@github-actions github-actions Bot added kind/feature Categorizes issue or PR as related to a new feature needs-triage needs-priority needs-actor release-note labels Jul 11, 2026
@gjkim42
gjkim42 marked this pull request as ready for review July 11, 2026 15:56

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 27 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="internal/controller/session_controller.go">

<violation number="1" location="internal/controller/session_controller.go:574">
P2: Sessions using a workspace-mounted `extraInitContainer` rerun that container after recovery, so migrations, resets, or seed scripts can overwrite preserved workspace state. Extend recovery guarding to supported user workspace setup or reject/document non-idempotent workspace init containers for Sessions.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread test/integration/session_test.go Outdated
Comment thread internal/cli/session.go
Comment thread internal/sessionruntime/server.go
break
}
}
if err := prepareSessionWorkspaceInit(podSpec.InitContainers); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Sessions using a workspace-mounted extraInitContainer rerun that container after recovery, so migrations, resets, or seed scripts can overwrite preserved workspace state. Extend recovery guarding to supported user workspace setup or reject/document non-idempotent workspace init containers for Sessions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/controller/session_controller.go, line 574:

<comment>Sessions using a workspace-mounted `extraInitContainer` rerun that container after recovery, so migrations, resets, or seed scripts can overwrite preserved workspace state. Extend recovery guarding to supported user workspace setup or reject/document non-idempotent workspace init containers for Sessions.</comment>

<file context>
@@ -476,6 +563,17 @@ func (r *SessionReconciler) buildSessionPod(session *kelos.Session, workspace *k
+			break
+		}
+	}
+	if err := prepareSessionWorkspaceInit(podSpec.InitContainers); err != nil {
+		return nil, nil, err
+	}
</file context>

Comment thread internal/controller/session_controller.go Outdated
Comment thread internal/sessionruntime/journal.go
Comment thread internal/sessionruntime/journal.go

@knechtionscoding knechtionscoding left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@gjkim42 this seems like an exceptional amount of code/work to handle what could be handled by having session pods use a stateful set?

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 12 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="internal/sessionruntime/server.go">

<violation number="1" location="internal/sessionruntime/server.go:306">
P1: When `journal.Append` fails in `submitMessage`, the turn was already enqueued on `s.turns` but `turn.accepted` is never closed. The `runTurns` goroutine then blocks forever on `<-turn.accepted`, preventing any further turns from being processed. This deadlocks the session runtime until the serve context is cancelled indirectly through journal failure detection.

Suggest closing `turn.accepted` before returning the error, or better, journal the event *before* enqueuing the turn so the turn is never sent unless the event is durably recorded.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread internal/sessionruntime/server.go Outdated
}
select {
case s.turns <- turn:
if err := s.journal.Append(Event{Type: EventUserMessage, RequestID: requestID, TurnID: turn.id, Text: turn.text}); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: When journal.Append fails in submitMessage, the turn was already enqueued on s.turns but turn.accepted is never closed. The runTurns goroutine then blocks forever on <-turn.accepted, preventing any further turns from being processed. This deadlocks the session runtime until the serve context is cancelled indirectly through journal failure detection.

Suggest closing turn.accepted before returning the error, or better, journal the event before enqueuing the turn so the turn is never sent unless the event is durably recorded.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/sessionruntime/server.go, line 306:

<comment>When `journal.Append` fails in `submitMessage`, the turn was already enqueued on `s.turns` but `turn.accepted` is never closed. The `runTurns` goroutine then blocks forever on `<-turn.accepted`, preventing any further turns from being processed. This deadlocks the session runtime until the serve context is cancelled indirectly through journal failure detection.

Suggest closing `turn.accepted` before returning the error, or better, journal the event *before* enqueuing the turn so the turn is never sent unless the event is durably recorded.</comment>

<file context>
@@ -295,7 +303,9 @@ func (s *Server) submitMessage(text string) error {
 	select {
 	case s.turns <- turn:
-		s.journal.Append(Event{Type: EventUserMessage, TurnID: turn.id, Text: turn.text})
+		if err := s.journal.Append(Event{Type: EventUserMessage, RequestID: requestID, TurnID: turn.id, Text: turn.text}); err != nil {
+			return fmt.Errorf("recording Session message: %w", err)
+		}
</file context>

@gjkim42 gjkim42 changed the title Recover sessions after pod loss Recover Sessions with StatefulSets Jul 12, 2026
@gjkim42
gjkim42 force-pushed the session-recovery branch 2 times, most recently from 036746f to f6fe792 Compare July 13, 2026 00:22
@gjkim42
gjkim42 force-pushed the session-recovery branch from f6fe792 to 0d4d2ec Compare July 13, 2026 01:13
@gjkim42
gjkim42 enabled auto-merge July 13, 2026 01:16
@gjkim42
gjkim42 added this pull request to the merge queue Jul 13, 2026
@gjkim42
gjkim42 removed this pull request from the merge queue due to a manual request Jul 13, 2026
@gjkim42
gjkim42 merged commit 59dc38b into main Jul 13, 2026
16 checks passed
@gjkim42
gjkim42 deleted the session-recovery branch July 13, 2026 01:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/feature Categorizes issue or PR as related to a new feature needs-actor needs-priority needs-triage release-note

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants