Skip to content

Send() blocks indefinitely when ReadyForInitialPrompt returns false despite Status() reporting "stable" #209

@johnstcn

Description

@johnstcn

Bug

POST /message with type: "user" hangs indefinitely when ReadyForInitialPrompt never returns true for the current screen, even though GET /status returns "stable".

Root cause

statusLocked() does not check initialPromptReady. It returns "stable" when the screen is stable and the queue is empty. Send() passes the status validation and enqueues the message. But the stableSignal (which the send loop waits on) requires initialPromptReady to be true. If ReadyForInitialPrompt never returns true, the signal never fires and Send() blocks forever.

statusLocked() = "stable"   →  Send() passes status check, enqueues
stableSignal requires:       →  initialPromptReady && queue > 0 && screenStable
initialPromptReady = false   →  signal never fires
Send() blocks on errCh       →  forever

Real-world trigger

Claude Code v2.1.87 shows a theme selection onboarding screen that uses ╌╌╌ (Unicode dashed lines) instead of ─── (box-drawing characters). The message box detection in findGreaterThanMessageBox / findGenericSlimMessageBox fails because it looks for ─────────────── and > which aren't present on this screen. initialPromptReady stays false and all user-type messages hang.

Last 6 lines of the actual screen content:

  1  function greet() {                                                         
  2 -  console.log("Hello, World!");                                            
  2 +  console.log("Hello, Claude!");                                           
  3  }                                                                          
 ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ 
  Syntax theme: Monokai Extended (ctrl+t to disable)                            

Note: (U+254C, BOX DRAWINGS LIGHT DOUBLE DASH HORIZONTAL) vs (U+2500, BOX DRAWINGS LIGHT HORIZONTAL).

Reproducing test

func TestSendBlocksWhenInitialPromptNotReady(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	t.Cleanup(cancel)

	mClock := quartz.NewMock(t)
	agent := &testAgent{screen: "onboarding screen without message box"}
	cfg := st.PTYConversationConfig{
		Clock:                 mClock,
		SnapshotInterval:      100 * time.Millisecond,
		ScreenStabilityLength: 200 * time.Millisecond,
		AgentIO:               agent,
		ReadyForInitialPrompt: func(screen string) bool {
			return false // Simulates failed message box detection.
		},
		Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
	}
	c := st.NewPTY(ctx, cfg, &testEmitter{})
	c.Start(ctx)

	// Fill snapshot buffer to reach stability.
	advanceFor(ctx, t, mClock, 300*time.Millisecond)

	// Status reports "stable" — Send() will pass the status check.
	assert.Equal(t, st.ConversationStatusStable, c.Status())

	// But Send() blocks forever because stableSignal requires
	// initialPromptReady, which is false.
	var sendDone atomic.Bool
	go func() {
		_ = c.Send(st.MessagePartText{Content: "hello"})
		sendDone.Store(true)
	}()
	advanceFor(ctx, t, mClock, 20*time.Second)

	assert.False(t, sendDone.Load(),
		"Send() blocks forever: status said stable but stableSignal never fires")
}

Possible fixes

  1. Make statusLocked() check initialPromptReady: return "initializing" or "changing" when initialPromptReady is false. This would cause Send() to reject the message with ErrMessageValidationChanging instead of hanging.
  2. Improve message box detection: add ╌╌╌ as an alternative to ─── in findGreaterThanMessageBox / findGenericSlimMessageBox.
  3. Both: fix the detection AND add the safety check in statusLocked().

Option 3 is likely the right approach — fix the detection for this specific Claude Code version, and add the statusLocked guard so future detection failures fail fast instead of hanging.

Discovered while smoke-testing #208.

🤖 This response was generated by Coder Agents.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions