Skip to content

agent stream never exits on a terminal execution: parseSSE has no terminal-event or idle termination #102

Description

@ambiorix2099

Summary

conductor agent stream <execution-id> never exits when the execution has already reached a
terminal state. It produces no output and hangs indefinitely until killed.

Any script, test, or CI job that calls agent stream will hang forever. This makes the command
impossible to automate and is why it is currently excluded from E2E coverage.

Found while regression-testing CLI branch cleanup/remove-skill-command (#100) against server
3.32.0-rc.23 on local OSS.

Reproduction

$ conductor agent run --config probe.yaml "Reply with just the word: pong"
Agent: e2e_probe_agent (Execution: 0507317e-6bea-4e79-9ad4-076cd7525df0)
  [thinking]
{"context":{},"finishReason":"STOP","result":"pong"}          # run exits correctly

$ conductor agent status 0507317e-6bea-4e79-9ad4-076cd7525df0
  "status": "COMPLETED",
  "isComplete": true,
  "isRunning": false,

$ conductor agent stream 0507317e-6bea-4e79-9ad4-076cd7525df0
# ...no output. Never returns. Confirmed still running at 15s, 20s, and 2min; killed manually.

Note agent run with inline streaming exits correctly — the server closes the stream when a
live execution finishes. Only the standalone stream command against an already-terminal
execution hangs.

Timing nuance: if agent stream is invoked within a few seconds of completion, the server still has
buffered events and the CLI replays them ([thinking], then the terminal done payload) — and
then hangs. Minutes later there is nothing to replay and it emits 0 bytes before hanging. Either
way it never terminates.

Root cause

Two things combine; the CLI-side one is the fix.

1. The server holds the connection open indefinitely. Raw SSE against a terminal execution
yields only comments, forever:

$ curl -sN -H 'Accept: text/event-stream' http://localhost:8080/api/agent/stream/<terminal-id>
:connected

:heartbeat
# ...connection stays open; still open at 15s

2. The client has no termination condition other than EOF. internal/agent/events.go:96:

for scanner.Scan() {          // returns only on body EOF or scan error
    line := scanner.Text()
    switch {
    case line == "":
        flush()
    case strings.HasPrefix(line, fieldComment):
        // comment / heartbeat — ignore          <-- events.go:101-102
    ...

parseSSE returns only when the HTTP body reaches EOF. There is no check for a terminal event
type
(EventDone / EventError) and no idle timeout. Heartbeat comments are explicitly
discarded, so they never even surface as events.

Because parseSSE never returns, restClient.Stream never closes the events channel
(internal/agent/client.go:223), so service.StreamExecution's for evt := range events
(internal/agent/service.go:91) blocks forever.

EventDone and EventError are already defined and already handled by the presentation layer
(cmd/agent_stream.go) — nothing consumes them as a stop signal.

Impact

  • agent stream cannot be used in any script or automated test — guaranteed hang.
  • Blocks E2E coverage of the agent streaming surface.
  • A user who streams a finished execution sees an apparently frozen terminal with no output and no
    error, and must Ctrl-C.

Fix options

A. Stop on terminal event (recommended). Treat EventDone and EventError as end-of-stream in
service.StreamExecution (or have parseSSE return after emitting one). The CLI should not depend
on the server closing the connection. Preserves --last-event-id resumption semantics for
long-lived streams.

B. Idle timeout. Return after N seconds with no non-comment event. Bounds the hang but is a
heuristic, and picks an arbitrary N.

C. Pre-flight status check. Query execution status first and skip streaming when already
terminal. Cheap, but racy — an execution can finish between the check and the stream — so it needs A
anyway.

D. Server-side close. Have the server close the SSE stream for terminal executions. Correct
complement to A, but the CLI still should not hang against an older or misbehaving server.

Recommend A, optionally with D upstream.

Test coverage

None today — agent has no E2E suite, which is partly because of this bug. Once fixed, an
agent.bats case should assert that agent stream <terminal-id> exits non-hanging with a bounded
timeout, so a regression shows up as a failure rather than a hung CI job.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions