From e01fa6bd9e854ea93c013c5cc5d6b192dd5ebecb Mon Sep 17 00:00:00 2001 From: james Date: Mon, 13 Apr 2026 08:59:34 -0500 Subject: [PATCH] fix(agent): prevent Claude runtime pings from hanging after the model has already finished Claude's stream-json flow can emit the terminal result event while the child process still waits on open stdin. Closing stdin as soon as the final result arrives lets the CLI exit cleanly instead of idling until the daemon timeout fires. Constraint: Must preserve the existing Claude stream-json protocol and child-process lifecycle Rejected: Increase ping timeout only | masks the hang without fixing process exit Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep Claude stdin handling aligned with the stream-json terminal result semantics; do not defer closure until goroutine teardown Tested: Reproduced self-hosted runtime ping timeout locally; verified ping succeeds after closing stdin on result; cd server && go test ./pkg/agent Not-tested: Full make check; Bedrock/Vertex-specific Claude auth flows --- server/pkg/agent/claude.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/server/pkg/agent/claude.go b/server/pkg/agent/claude.go index 3c5c19a94..ae75c1c18 100644 --- a/server/pkg/agent/claude.go +++ b/server/pkg/agent/claude.go @@ -52,14 +52,21 @@ func (b *claudeBackend) Execute(ctx context.Context, prompt string, opts ExecOpt cancel() return nil, fmt.Errorf("claude stdin pipe: %w", err) } + closeStdin := func() { + if stdin != nil { + _ = stdin.Close() + stdin = nil + } + } cmd.Stderr = newLogWriter(b.cfg.Logger, "[claude:stderr] ") if err := cmd.Start(); err != nil { + closeStdin() cancel() return nil, fmt.Errorf("start claude: %w", err) } if err := writeClaudeInput(stdin, prompt); err != nil { - _ = stdin.Close() + closeStdin() cancel() _ = cmd.Wait() return nil, fmt.Errorf("write claude input: %w", err) @@ -70,7 +77,7 @@ func (b *claudeBackend) Execute(ctx context.Context, prompt string, opts ExecOpt resCh := make(chan Result, 1) go func() { - defer stdin.Close() + defer closeStdin() defer cancel() defer close(msgCh) defer close(resCh) @@ -107,6 +114,7 @@ func (b *claudeBackend) Execute(ctx context.Context, prompt string, opts ExecOpt } trySend(msgCh, Message{Type: MessageStatus, Status: "running"}) case "result": + closeStdin() sessionID = msg.SessionID if msg.ResultText != "" { output.Reset()