Skip to content

Keep kelos logs -f open after empty streams - #1654

Merged
gjkim42 merged 1 commit into
mainfrom
kelos-task-1653
Aug 15, 2026
Merged

gjkim42 merged 1 commit into
mainfrom
kelos-task-1653

Conversation

@gjkim42

@gjkim42 gjkim42 commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator

What type of PR is this?

/kind bug

What this PR does / why we need it:

Kubernetes can successfully open an agent log stream and immediately return EOF while the Task and Pod are still Pending. The CLI previously treated that empty stream as successful completion, so kelos logs <task> -f could exit before the Task produced any output.

This change probes successful agent follow streams before returning them to the log parser. If a stream is empty, the CLI checks the current Task, Pod, and container state and reconnects while the Task remains active. Terminal Tasks end promptly, while failed Tasks and non-transient container startup failures return actionable errors. Non-empty streams continue through the existing parser without replaying output.

Which issue(s) this PR is related to:

Fixes #1653

Special notes for your reviewer:

The retry path applies only to empty agent follow streams. Init-container and non-follow log behavior remain unchanged.

Validation:

  • env -u CODEX_HOME -u CODEX_AUTH_JSON make test
  • make verify
  • make build
  • git diff --check

The E2E suite is left to PR CI as directed by the project guidance.

Does this PR introduce a user-facing change?

`kelos logs -f` now reconnects when Kubernetes closes an empty agent log stream before the Task completes.

Summary by cubic

Keep kelos logs -f open when Kubernetes returns an empty agent log stream so the CLI does not exit before Task output appears. Previously an empty stream meant success; now the CLI reconnects while the Task remains active and returns actionable errors for terminal or non-transient startup failures.

Reviewer notes

  • Applies only to empty follow streams from the agent container; non-follow and init-container behavior is unchanged.
  • Peeks one byte with a buffered reader and wraps the stream in a bufferedReadCloser to avoid replaying or dropping data when the stream is non-empty.
  • Reconnect decisions come from shouldRetryEmptyTaskLogStream(...), which inspects Task, Pod, and container state; it retries for active states and fails with clear messages for TaskFailed, container waiting reasons (e.g., ImagePullBackOff), or PodFailed/Unknown.
  • Updates docs/reference.md to describe the reconnection behavior and adds tests for retry, terminal, and failure paths.

Written for commit 49b7b4a. Summary will update on new commits.

Review in cubic

@gjkim42
gjkim42 marked this pull request as ready for review August 15, 2026 11:51
@gjkim42

gjkim42 commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

/kelos review

@kelos-bot

kelos-bot Bot commented Aug 15, 2026 •

Copy link
Copy Markdown
Contributor

🤖 Kelos Task Status

Task kelos-reviewer-issue-comment-87bace42b362 has succeeded. ✅

@kelos-bot

kelos-bot Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

🤖 Kelos Reviewer Agent @gjkim42

Review Summary

Verdict: APPROVE (sticky comment only)
Overall correctness: patch is correct
Scope: Reconnect empty agent follow streams while the Task remains active, with state-aware terminal errors, focused tests, and CLI reference documentation.

Findings Overview

Priority Count File:Line Summary
P0 0 — none
P1 0 — none
P2 0 — none
P3 0 — none

Findings

No qualifying findings.

Key takeaways

  • The empty-stream retry is limited to agent-container follow mode, preserving init-container and non-follow behavior.
  • Buffered probing preserves the first byte of non-empty streams, and the tests cover retry, terminal, and actionable failure paths.

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

3 issues found across 3 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/cli/logs.go">

<violation number="1" location="internal/cli/logs.go:416">
P2: When the Pod has already failed but the Task status has not caught up, this branch treats the empty stream as transient and skips the `PodFailed` error below. Check `pod.Status.Phase == corev1.PodFailed` before iterating container statuses, or handle failed terminated containers explicitly, so `logs -f` reports the failure instead of retrying indefinitely.</violation>

<violation number="2" location="internal/cli/logs.go:416">
P2: When the agent container has terminated with a non-zero exit code, this branch returns retry=true (because `status.State.Terminated` is non-nil) without inspecting the exit code, and it returns before the Task/Pod failure checks. The CLI then reconnects on an empty stream every retryInterval instead of reporting the failure, until the Task phase happens to flip to Failed. Treat a non-zero `Terminated.ExitCode` as a failure so it yields the actionable error the PR/documented behavior promises, and add a test for a terminated-with-error container.</violation>
</file>

<file name="internal/cli/logs_test.go">

<violation number="1" location="internal/cli/logs_test.go:209">
P3: The new tests cover the main retry/terminal/failure paths but leave three branches of `shouldRetryEmptyTaskLogStream` unvalidated: the pod-level `PodFailed` message error, the `PodUnknown` error, and the transient `Waiting` reasons (PodInitializing/ContainerCreating) that should keep retrying. These are new decision branches in this PR; add a case for each so the empty-stream retry logic is fully verified.</violation>
</file>

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

Re-trigger cubic

Comment thread internal/cli/logs.go
switch {
case status.State.Running != nil:
return true, nil
case status.State.Terminated != 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: When the Pod has already failed but the Task status has not caught up, this branch treats the empty stream as transient and skips the PodFailed error below. Check pod.Status.Phase == corev1.PodFailed before iterating container statuses, or handle failed terminated containers explicitly, so logs -f reports the failure instead of retrying indefinitely.

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

<comment>When the Pod has already failed but the Task status has not caught up, this branch treats the empty stream as transient and skips the `PodFailed` error below. Check `pod.Status.Phase == corev1.PodFailed` before iterating container statuses, or handle failed terminated containers explicitly, so `logs -f` reports the failure instead of retrying indefinitely.</comment>

<file context>
@@ -349,6 +384,71 @@ func openTaskLogStream(
+		switch {
+		case status.State.Running != nil:
+			return true, nil
+		case status.State.Terminated != nil:
+			return true, nil
+		case status.State.Waiting == nil:
</file context>

Comment thread internal/cli/logs.go
Comment on lines +416 to +417
case status.State.Terminated != nil:
return true, 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: When the agent container has terminated with a non-zero exit code, this branch returns retry=true (because status.State.Terminated is non-nil) without inspecting the exit code, and it returns before the Task/Pod failure checks. The CLI then reconnects on an empty stream every retryInterval instead of reporting the failure, until the Task phase happens to flip to Failed. Treat a non-zero Terminated.ExitCode as a failure so it yields the actionable error the PR/documented behavior promises, and add a test for a terminated-with-error container.

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

<comment>When the agent container has terminated with a non-zero exit code, this branch returns retry=true (because `status.State.Terminated` is non-nil) without inspecting the exit code, and it returns before the Task/Pod failure checks. The CLI then reconnects on an empty stream every retryInterval instead of reporting the failure, until the Task phase happens to flip to Failed. Treat a non-zero `Terminated.ExitCode` as a failure so it yields the actionable error the PR/documented behavior promises, and add a test for a terminated-with-error container.</comment>

<file context>
@@ -349,6 +384,71 @@ func openTaskLogStream(
+		switch {
+		case status.State.Running != nil:
+			return true, nil
+		case status.State.Terminated != nil:
+			return true, nil
+		case status.State.Waiting == nil:
</file context>
Suggested change
case status.State.Terminated != nil:
return true, nil
case status.State.Terminated != nil:
if status.State.Terminated.ExitCode != 0 {
return false, fmt.Errorf(
"container %q in pod %q terminated with exit %d while following task %q logs",
container,
podName,
status.State.Terminated.ExitCode,
taskName,
)
}
return true, nil

Comment thread internal/cli/logs_test.go
}
}

func TestOpenTaskLogStreamRetriesEmptyStreamWhileTaskIsPending(t *testing.T) {

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: The new tests cover the main retry/terminal/failure paths but leave three branches of shouldRetryEmptyTaskLogStream unvalidated: the pod-level PodFailed message error, the PodUnknown error, and the transient Waiting reasons (PodInitializing/ContainerCreating) that should keep retrying. These are new decision branches in this PR; add a case for each so the empty-stream retry logic is fully verified.

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

<comment>The new tests cover the main retry/terminal/failure paths but leave three branches of `shouldRetryEmptyTaskLogStream` unvalidated: the pod-level `PodFailed` message error, the `PodUnknown` error, and the transient `Waiting` reasons (PodInitializing/ContainerCreating) that should keep retrying. These are new decision branches in this PR; add a case for each so the empty-stream retry logic is fully verified.</comment>

<file context>
@@ -206,6 +206,237 @@ func TestOpenTaskLogStreamRetriesWhilePodIsStarting(t *testing.T) {
 	}
 }
 
+func TestOpenTaskLogStreamRetriesEmptyStreamWhileTaskIsPending(t *testing.T) {
+	task := &kelos.Task{
+		ObjectMeta: metav1.ObjectMeta{Name: "task-1", Namespace: "default"},
</file context>

@gjkim42
gjkim42 added this pull request to the merge queue Aug 15, 2026
Merged via the queue into main with commit 36714ea Aug 15, 2026
32 checks passed
@gjkim42
gjkim42 deleted the kelos-task-1653 branch August 15, 2026 12:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

kelos logs -f can exit before a Pending task produces results

1 participant