@@ -26,6 +26,8 @@ import (
26 "github.com/charmbracelet/crush/internal/shell"
26 "github.com/charmbracelet/crush/internal/shell"
29+const streamChunkTimeout = 80 * time.Second
29type AgentEventType string
31type AgentEventType string
@@ -553,16 +555,25 @@ func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msg
553 ctx = context.WithValue(ctx, tools.MessageIDContextKey, assistantMsg.ID)
555 ctx = context.WithValue(ctx, tools.MessageIDContextKey, assistantMsg.ID)
555 // Process each event in the stream.
557 // Process each event in the stream.
556− for event := range eventChan {
561+ case event, ok := <-eventChan:
557− if processErr := a.processEvent(ctx, sessionID, &assistantMsg, event); processErr != nil {
562+ if !ok {
558− if errors.Is(processErr, context.Canceled) {
563+ break loop
559− a.finishMessage(context.Background(), &assistantMsg, message.FinishReasonCanceled, "Request cancelled", "")
561− a.finishMessage(ctx, &assistantMsg, message.FinishReasonError, "API Error", processErr.Error())
563− return assistantMsg, nil, processErr
565+ if processErr := a.processEvent(ctx, sessionID, &assistantMsg, event); processErr != nil {
564− }
566+ if errors.Is(processErr, context.Canceled) {
565− if ctx.Err() != nil {
567+ a.finishMessage(context.Background(), &assistantMsg, message.FinishReasonCanceled, "Request cancelled", "")
569+ a.finishMessage(ctx, &assistantMsg, message.FinishReasonError, "API Error", processErr.Error())
571+ return assistantMsg, nil, processErr
573+ case <-time.After(streamChunkTimeout):
574+ a.finishMessage(ctx, &assistantMsg, message.FinishReasonError, "Stream timeout", "No chunk received within timeout")
575+ return assistantMsg, nil, fmt.Errorf("stream chunk timeout")
566 a.finishMessage(context.Background(), &assistantMsg, message.FinishReasonCanceled, "Request cancelled", "")
577 a.finishMessage(context.Background(), &assistantMsg, message.FinishReasonCanceled, "Request cancelled", "")
567 return assistantMsg, nil, ctx.Err()
578 return assistantMsg, nil, ctx.Err()