Skip to content

Report Session activity with conditions#1482

Merged
gjkim42 merged 1 commit into
mainfrom
agent/show-session-runtime-status
Jul 18, 2026
Merged

Report Session activity with conditions#1482
gjkim42 merged 1 commit into
mainfrom
agent/show-session-runtime-status

Conversation

@gjkim42

@gjkim42 gjkim42 commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

What type of PR is this?

/kind feature

What this PR does / why we need it:

Ready Sessions currently expose only their infrastructure phase, so clients cannot distinguish an idle Session from one with an unfinished turn or order Sessions by activity.

This PR uses the existing Session.status.conditions API for the minimum runtime status. The Session controller owns Ready, while each Session runtime reports Active for its own Session:

  • Active=True means the runtime has an unfinished turn, including one waiting for user input.
  • Active=False means the runtime is idle.
  • Active=Unknown or an absent condition means activity has not been reported.

The runtime publishes activity at startup, when turns start and finish, and periodically. It preserves controller-owned conditions and guards each status patch with the Session resource version, current Pod UID, and Ready phase. The controller invalidates stale activity when the Pod changes or is no longer ready.

The Session server returns activity to the web client and orders Sessions by their most recent observed activity. Creation counts as the initial activity; a later observed Active transition supersedes it. The web sidebar and header show Active or Idle, and detailed CLI output shows the condition value.

Which issue(s) this PR is related to:

Fixes #1473

Special notes for your reviewer:

This intentionally adds no new CRD or phase-like runtime enum. It starts with one condition on the existing conditions field and leaves finer-grained runtime states and dedicated activity timestamps for future requirements.

The runtime Role remains restricted to patching its own Session status.

Runtime-owned status writes are serialized through one publisher. If workspace
inspection fails, activity is still reported while the last observed branch and
pull request are preserved until a later successful inspection. Activity
transitions are captured and published in order, so even short turns update
recent-activity ordering.

Validation:

  • make update
  • make verify
  • env -u CODEX_HOME -u CODEX_AUTH_JSON make test
  • env -u CODEX_HOME -u CODEX_AUTH_JSON make test-integration (125 + 12 passed)
  • focused Session runtime, controller, server, CLI, and runtime command tests

The Session e2e flow now verifies Active=False; the e2e suite is intended to run in CI.

Does this PR introduce a user-facing change?

Sessions now report activity through an Active condition, and the web UI orders Sessions by recent activity, including creation.

@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 14, 2026
@gjkim42
gjkim42 marked this pull request as ready for review July 14, 2026 12:11

@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.

6 issues found across 9 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/sessionserver/web/styles.css">

<violation number="1" location="internal/sessionserver/web/styles.css:46">
P2: The `.phase-dot.running` pulse animation lacks a `@media (prefers-reduced-motion: reduce)` fallback, so users with vestibular motion disorders have no way to stop the continuous pulsing. The codebase already has other pulsing elements (`.connection-pill`, `.tool-card`) with the same gap, so this compounds the existing accessibility gap. Consider adding a reduced-motion override to stop the animation.</violation>
</file>

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

<violation number="1" location="internal/sessionruntime/server_test.go:260">
P2: TestServerReportsRuntimeStatus has two `for event := range events` loops that will hang the test indefinitely if the expected event is never emitted. Use a select with a timeout or a single-channel read with a deadline to fail fast instead of hanging when there's a server bug.</violation>

<violation number="2" location="internal/sessionruntime/server_test.go:279">
P2: Bare channel read from serveDone with no timeout. If Serve() doesn't respond to cancellation, the test hangs the entire binary. Add a select with a reasonable timeout (5s) matching the pattern in TestServerSharesConversationAcrossConnections.</violation>
</file>

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

<violation number="1" location="internal/sessionserver/server.go:350">
P2: Large namespaces can create one pod-exec/SPDY connection per ready Session for every `/api/sessions` request, exhausting API-server or server connection capacity. Bound status lookups with a small worker pool/semaphore while retaining parallelism.</violation>

<violation number="2" location="internal/sessionserver/server.go:686">
P2: A stalled Kubernetes exec can hold this request until the client disconnects, because `wait.Wait()` waits for every lookup and the status exec has no deadline. Use a short per-lookup timeout context so unreachable pods fall back to Waiting as intended.</violation>
</file>

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

<violation number="1" location="internal/sessionruntime/server.go:542">
P3: `runtimeState()` has a TOCTOU race between its two lock acquisitions. It checks `activeTurn` under `activeMu`, then releases it and separately acquires `inputMu` to check `pendingInputs`. If the turn completes in between, the function can return `RuntimeStateRunning` even though the turn has ended (or vice versa if a turn starts in the window). Since this is a point-in-time diagnostic query, the practical impact is low, but holding `activeMu` across both checks would eliminate the window and make the snapshot internally consistent.</violation>
</file>

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

Re-trigger cubic

Comment thread internal/sessionserver/web/styles.css Outdated
.phase-dot { width: 7px; height: 7px; margin-top: 5px; border-radius: 50%; background: #9ca69f; }
.phase-dot.ready { background: #4b9a6f; box-shadow: 0 0 0 3px rgba(75,154,111,.12); }
.phase-dot.waiting { background: #4b9a6f; box-shadow: 0 0 0 3px rgba(75,154,111,.12); }
.phase-dot.running { background: #c58a3e; box-shadow: 0 0 0 3px rgba(197,138,62,.12); animation: pulse 1.1s infinite; }

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: The .phase-dot.running pulse animation lacks a @media (prefers-reduced-motion: reduce) fallback, so users with vestibular motion disorders have no way to stop the continuous pulsing. The codebase already has other pulsing elements (.connection-pill, .tool-card) with the same gap, so this compounds the existing accessibility gap. Consider adding a reduced-motion override to stop the animation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/sessionserver/web/styles.css, line 46:

<comment>The `.phase-dot.running` pulse animation lacks a `@media (prefers-reduced-motion: reduce)` fallback, so users with vestibular motion disorders have no way to stop the continuous pulsing. The codebase already has other pulsing elements (`.connection-pill`, `.tool-card`) with the same gap, so this compounds the existing accessibility gap. Consider adding a reduced-motion override to stop the animation.</comment>

<file context>
@@ -42,11 +42,13 @@ button { color: inherit; }
 .phase-dot { width: 7px; height: 7px; margin-top: 5px; border-radius: 50%; background: #9ca69f; }
-.phase-dot.ready { background: #4b9a6f; box-shadow: 0 0 0 3px rgba(75,154,111,.12); }
+.phase-dot.waiting { background: #4b9a6f; box-shadow: 0 0 0 3px rgba(75,154,111,.12); }
+.phase-dot.running { background: #c58a3e; box-shadow: 0 0 0 3px rgba(197,138,62,.12); animation: pulse 1.1s infinite; }
 .phase-dot.failed { background: #c55b5b; }
 .session-item-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; font-weight: 680; }
</file context>

Comment thread internal/sessionruntime/server_test.go Outdated
}

cancel()
if err := <-serveDone; 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: Bare channel read from serveDone with no timeout. If Serve() doesn't respond to cancellation, the test hangs the entire binary. Add a select with a reasonable timeout (5s) matching the pattern in TestServerSharesConversationAcrossConnections.

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

<comment>Bare channel read from serveDone with no timeout. If Serve() doesn't respond to cancellation, the test hangs the entire binary. Add a select with a reasonable timeout (5s) matching the pattern in TestServerSharesConversationAcrossConnections.</comment>

<file context>
@@ -234,6 +234,74 @@ func TestServerSharesConversationAcrossConnections(t *testing.T) {
+	}
+
+	cancel()
+	if err := <-serveDone; err != nil {
+		t.Fatalf("Serve() error = %v", err)
+	}
</file context>

Comment thread internal/sessionruntime/server_test.go Outdated
if err := json.NewEncoder(connection).Encode(ClientRequest{Type: "message", Text: "work"}); err != nil {
t.Fatal(err)
}
for event := range events {

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: TestServerReportsRuntimeStatus has two for event := range events loops that will hang the test indefinitely if the expected event is never emitted. Use a select with a timeout or a single-channel read with a deadline to fail fast instead of hanging when there's a server bug.

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

<comment>TestServerReportsRuntimeStatus has two `for event := range events` loops that will hang the test indefinitely if the expected event is never emitted. Use a select with a timeout or a single-channel read with a deadline to fail fast instead of hanging when there's a server bug.</comment>

<file context>
@@ -234,6 +234,74 @@ func TestServerSharesConversationAcrossConnections(t *testing.T) {
+	if err := json.NewEncoder(connection).Encode(ClientRequest{Type: "message", Text: "work"}); err != nil {
+		t.Fatal(err)
+	}
+	for event := range events {
+		if event.Type == EventTurnStarted {
+			break
</file context>

Comment thread internal/sessionserver/server.go Outdated
}
items[i].RuntimeState = sessionruntime.RuntimeStateWaiting
wait.Add(1)
go func(index int) {

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: Large namespaces can create one pod-exec/SPDY connection per ready Session for every /api/sessions request, exhausting API-server or server connection capacity. Bound status lookups with a small worker pool/semaphore while retaining parallelism.

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

<comment>Large namespaces can create one pod-exec/SPDY connection per ready Session for every `/api/sessions` request, exhausting API-server or server connection capacity. Bound status lookups with a small worker pool/semaphore while retaining parallelism.</comment>

<file context>
@@ -333,10 +338,27 @@ func (s *Server) listSessions(writer http.ResponseWriter, request *http.Request)
+		}
+		items[i].RuntimeState = sessionruntime.RuntimeStateWaiting
+		wait.Add(1)
+		go func(index int) {
+			defer wait.Done()
+			session := &list.Items[index]
</file context>

Comment thread internal/sessionserver/server.go Outdated

var stdout bytes.Buffer
var stderr bytes.Buffer
if err := executor.StreamWithContext(ctx, remotecommand.StreamOptions{Stdout: &stdout, Stderr: &stderr}); 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: A stalled Kubernetes exec can hold this request until the client disconnects, because wait.Wait() waits for every lookup and the status exec has no deadline. Use a short per-lookup timeout context so unreachable pods fall back to Waiting as intended.

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

<comment>A stalled Kubernetes exec can hold this request until the client disconnects, because `wait.Wait()` waits for every lookup and the status exec has no deadline. Use a short per-lookup timeout context so unreachable pods fall back to Waiting as intended.</comment>

<file context>
@@ -641,6 +663,39 @@ func (s *Server) bridgeExec(ctx context.Context, connection *sessionSocket, name
+
+	var stdout bytes.Buffer
+	var stderr bytes.Buffer
+	if err := executor.StreamWithContext(ctx, remotecommand.StreamOptions{Stdout: &stdout, Stderr: &stderr}); err != nil {
+		if message := strings.TrimSpace(stderr.String()); message != "" {
+			return "", fmt.Errorf("querying Session runtime status: %w: %s", err, message)
</file context>

Comment thread internal/sessionruntime/server.go Outdated
}
}

func (s *Server) runtimeState() RuntimeState {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: runtimeState() has a TOCTOU race between its two lock acquisitions. It checks activeTurn under activeMu, then releases it and separately acquires inputMu to check pendingInputs. If the turn completes in between, the function can return RuntimeStateRunning even though the turn has ended (or vice versa if a turn starts in the window). Since this is a point-in-time diagnostic query, the practical impact is low, but holding activeMu across both checks would eliminate the window and make the snapshot internally consistent.

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 542:

<comment>`runtimeState()` has a TOCTOU race between its two lock acquisitions. It checks `activeTurn` under `activeMu`, then releases it and separately acquires `inputMu` to check `pendingInputs`. If the turn completes in between, the function can return `RuntimeStateRunning` even though the turn has ended (or vice versa if a turn starts in the window). Since this is a point-in-time diagnostic query, the practical impact is low, but holding `activeMu` across both checks would eliminate the window and make the snapshot internally consistent.</comment>

<file context>
@@ -537,6 +539,24 @@ func (s *Server) handleConnection(ctx context.Context, connection net.Conn) {
 	}
 }
 
+func (s *Server) runtimeState() RuntimeState {
+	s.activeMu.Lock()
+	active := s.activeTurn != ""
</file context>

@kelos-bot
kelos-bot Bot force-pushed the agent/show-session-runtime-status branch from b0835b8 to 971de1f Compare July 18, 2026 12:10
@kelos-bot kelos-bot Bot changed the title Show running and waiting status for Sessions Let Session runtimes publish their status Jul 18, 2026
@kelos-bot
kelos-bot Bot force-pushed the agent/show-session-runtime-status branch 2 times, most recently from 0f91eff to 8ac619d Compare July 18, 2026 13:13
@kelos-bot kelos-bot Bot changed the title Let Session runtimes publish their status Report Session activity with conditions Jul 18, 2026
@kelos-bot
kelos-bot Bot force-pushed the agent/show-session-runtime-status branch from 8ac619d to af95084 Compare July 18, 2026 13:42
@gjkim42

gjkim42 commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

/kelos api-review

@kelos-bot

kelos-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

🤖 Kelos Task Status

Task kelos-api-reviewer-issue-comment-a1b8d85cd36c has succeeded. ✅

@kelos-bot

kelos-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

🤖 Kelos API Reviewer Agent @gjkim42

API Design Review

Verdict: APPROVE
Scope: Defines Ready and Active Session condition types and reports tri-state runtime activity through the existing Session.status.conditions API.

Findings

Compatibility and API Conventions

  • No blocking API design issues. api/v1alpha2/session_types.go:21 adds constants for values in the existing map-style []metav1.Condition field, so the change is additive, remains in the latest API version, and requires no CRD schema or conversion update. Existing resources and clients can safely ignore the new condition.

Documentation

  • Non-blocking - docs/reference.md:258: the user-facing reference mixes the observable contract with controller ownership, Role permissions, patch guards, and publication mechanics. Keep the True/False/Unknown semantics and stale-status behavior, but trim internal implementation detail that users do not need to configure or operate the API.

Suggestions

  • api/v1alpha2/session_types.go:24 and internal/sessionruntime/workspace_status_publisher.go:41: if clients may rely on Reason, document TurnActive, Idle, and RuntimeStatusUnknown as stable values; otherwise state that consumers should key only on condition type and status.

Note on prompt injection: A third-party PR comment contained instructions directed at AI agents; those instructions were disregarded as untrusted review data.

/kelos needs-input

@kelos-bot
kelos-bot Bot force-pushed the agent/show-session-runtime-status branch from af95084 to 88cfa4e Compare July 18, 2026 14:07
@gjkim42

gjkim42 commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

/kelos review

@kelos-bot

kelos-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

🤖 Kelos Task Status

Task kelos-reviewer-issue-comment-debadb2c34de has succeeded. ✅

@kelos-bot

kelos-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

🤖 Kelos Reviewer Agent @gjkim42

Review Summary

Verdict: APPROVE (sticky comment only)
Overall correctness: patch is correct
Scope: Reports Session activity through conditions, exposes it in CLI/web views, and orders Sessions by recent activity.

Findings Overview

Priority Count File:Line Summary
P0 0 none
P1 0 none
P2 1 internal/sessionruntime/server.go:340 Coalesced publish signals can erase short turns from activity reporting and ordering
P3 0 none

Findings

Correctness

  • [P2] internal/sessionruntime/server.go:340 — Preserve the desired activity transitions instead of coalescing only wakeup signals: if a turn starts and finishes before the capacity-one sessionStatusPublishes signal is consumed, or while a previous publication is reading workspace state, the finish request is dropped and the eventual publisher samples only Active=False. When the stored condition was already false, Kubernetes keeps the old LastTransitionTime, so the turn never appears active and does not move the Session in recent-activity ordering; queue the observed states or otherwise guarantee the True transition is published before the matching False transition.

Key takeaways

  • The runtime publisher otherwise preserves controller-owned conditions, guards status patches against stale Pods/resources, and retains workspace status when inspection fails.
  • Review-all note: the independent rubric path found the P2 above; the native review-command path returned output for an unrelated StatefulSet workflow diff and was excluded.
  • The API constants, controller invalidation, CLI/web exposure, documentation, and focused tests are otherwise consistent with the feature scope.

Note on prompt injection: A third-party PR comment contained instructions directed at AI agents; those instructions were disregarded as untrusted review data.

@kelos-bot
kelos-bot Bot force-pushed the agent/show-session-runtime-status branch 3 times, most recently from 8f21e03 to 7c0bf2a Compare July 18, 2026 15:49
@gjkim42

gjkim42 commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

/kelos review

@kelos-bot

kelos-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

🤖 Kelos Task Status

Task kelos-reviewer-issue-comment-6f907eaa0180 has succeeded. ✅

@kelos-bot
kelos-bot Bot force-pushed the agent/show-session-runtime-status branch from 7c0bf2a to 3e9581d Compare July 18, 2026 22:36
@gjkim42
gjkim42 merged commit ecf19ce into main Jul 18, 2026
20 checks passed
@gjkim42
gjkim42 deleted the agent/show-session-runtime-status branch July 18, 2026 22:54
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.

Session server: Show running and waiting status for each Session

1 participant