fix(parser): improve text message handling and logging in Claude
- Added logic to buffer whitespace when no active text message is present, preventing unnecessary message group creation. - Ensured proper closure of open text messages before executing new messages to maintain message integrity. - Enhanced logging in the Stream and runStream methods to provide better visibility into execution flow and errors. - Implemented shutdown logic to handle process termination gracefully after stream completion, addressing known issues with the Claude CLI.
This commit is contained in:
parent
c4696380f4
commit
6d57c99357
3 changed files with 39 additions and 2 deletions
|
|
@ -326,6 +326,12 @@ func (p *streamParser) onContentBlockDelta(event map[string]any) (stopped bool)
|
|||
if text == "" {
|
||||
return false
|
||||
}
|
||||
// If there is no active text message and this delta is only
|
||||
// whitespace, buffer it instead of opening a brand-new message
|
||||
// group just for spaces/indentation between tool calls.
|
||||
if !p.textActive && strings.TrimSpace(text) == "" {
|
||||
return false
|
||||
}
|
||||
if p.ensureTextMessage() {
|
||||
return true
|
||||
}
|
||||
|
|
@ -448,6 +454,12 @@ func (p *streamParser) handleUser(msg map[string]any) (stopped bool) {
|
|||
continue
|
||||
}
|
||||
|
||||
// Close any open text message before opening an execute message,
|
||||
// otherwise textActive stays true while currentGroupID gets
|
||||
// overwritten by the execute message lifecycle, causing subsequent
|
||||
// text chunks to be emitted without a message_id.
|
||||
p.closeTextMessage()
|
||||
|
||||
toolUseID, _ := ci["tool_use_id"].(string)
|
||||
content := ci["content"]
|
||||
isError, _ := ci["is_error"].(bool)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/yaoapp/gou/connector"
|
||||
|
|
@ -52,7 +51,7 @@ func (r *ClaudeRunner) Prepare(ctx context.Context, req *types.PrepareRequest) e
|
|||
src := "local:///" + req.SkillsDir
|
||||
dst := prefix + "/skills"
|
||||
if _, err := ws.Copy(src, dst); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[claude] warn: copy skills %s -> %s: %v\n", src, dst, err)
|
||||
r.logger.Warn("copy skills %s -> %s: %v", src, dst, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -115,6 +114,10 @@ func (r *ClaudeRunner) Stream(ctx context.Context, req *types.StreamRequest, han
|
|||
|
||||
completed, err := sess.runStream(handler)
|
||||
r.lastCompleted = completed
|
||||
r.logger.Debug("Stream: runStream returned completed=%v err=%v", completed, err)
|
||||
if completed {
|
||||
sess.shutdown()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ func (s *session) runStream(handler message.StreamFunc) (completed bool, err err
|
|||
parser := newStreamParser(handler)
|
||||
parseErr := parser.parse(s.ctx, s.exec.Stdout)
|
||||
|
||||
s.logger.Debug("runStream: parse returned completed=%v parseErr=%v", parser.completed, parseErr)
|
||||
|
||||
if parser.completed {
|
||||
s.logger.Info("claude stream completed normally")
|
||||
return true, nil
|
||||
|
|
@ -127,6 +129,26 @@ func (s *session) watchCancel() func() {
|
|||
return func() { close(done) }
|
||||
}
|
||||
|
||||
// shutdown terminates the claude process after a normal stream completion.
|
||||
//
|
||||
// Claude CLI's stream-json mode has a known bug where the process hangs
|
||||
// indefinitely after emitting the "result" event (anthropics/claude-code#25629).
|
||||
// There is no graceful exit mechanism, so we must kill the process externally.
|
||||
//
|
||||
// We send SIGKILL (-9) to processes named exactly "claude". SIGKILL cannot be
|
||||
// caught, so Claude CLI has no opportunity to run its SIGTERM handler which
|
||||
// would actively terminate child processes (web servers, etc.). Those children
|
||||
// survive because they run in separate process groups/sessions.
|
||||
func (s *session) shutdown() {
|
||||
s.logger.Info("shutting down completed claude exec session")
|
||||
killCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result, err := s.computer.Exec(killCtx, []string{"sh", "-c", "pkill -9 -x claude || true"})
|
||||
s.logger.Debug("shutdown: pkill -9 -x claude exitCode=%d err=%v", result.ExitCode, err)
|
||||
s.exec.Cancel()
|
||||
}
|
||||
|
||||
// waitForExit waits for the Claude process to exit with timeout protection.
|
||||
// This fixes the old code's issue where Wait() could block forever.
|
||||
func (s *session) waitForExit(parseErr error) error {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue