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.
Summary
conductor agent stream <execution-id>never exits when the execution has already reached aterminal state. It produces no output and hangs indefinitely until killed.
Any script, test, or CI job that calls
agent streamwill hang forever. This makes the commandimpossible to automate and is why it is currently excluded from E2E coverage.
Found while regression-testing CLI branch
cleanup/remove-skill-command(#100) against server3.32.0-rc.23 on local OSS.
Reproduction
Note
agent runwith inline streaming exits correctly — the server closes the stream when alive execution finishes. Only the standalone
streamcommand against an already-terminalexecution hangs.
Timing nuance: if
agent streamis invoked within a few seconds of completion, the server still hasbuffered events and the CLI replays them (
[thinking], then the terminaldonepayload) — andthen 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:
2. The client has no termination condition other than EOF.
internal/agent/events.go:96:parseSSEreturns only when the HTTP body reaches EOF. There is no check for a terminal eventtype (
EventDone/EventError) and no idle timeout. Heartbeat comments are explicitlydiscarded, so they never even surface as events.
Because
parseSSEnever returns,restClient.Streamnever closes theeventschannel(
internal/agent/client.go:223), soservice.StreamExecution'sfor evt := range events(
internal/agent/service.go:91) blocks forever.EventDoneandEventErrorare already defined and already handled by the presentation layer(
cmd/agent_stream.go) — nothing consumes them as a stop signal.Impact
agent streamcannot be used in any script or automated test — guaranteed hang.error, and must Ctrl-C.
Fix options
A. Stop on terminal event (recommended). Treat
EventDoneandEventErroras end-of-stream inservice.StreamExecution(or haveparseSSEreturn after emitting one). The CLI should not dependon the server closing the connection. Preserves
--last-event-idresumption semantics forlong-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 —
agenthas no E2E suite, which is partly because of this bug. Once fixed, anagent.batscase should assert thatagent stream <terminal-id>exits non-hanging with a boundedtimeout, so a regression shows up as a failure rather than a hung CI job.