-
Notifications
You must be signed in to change notification settings - Fork 114
Send() blocks indefinitely when ReadyForInitialPrompt returns false despite Status() reporting "stable" #209
Description
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
- Make
statusLocked()checkinitialPromptReady: return"initializing"or"changing"wheninitialPromptReadyis false. This would causeSend()to reject the message withErrMessageValidationChanginginstead of hanging. - Improve message box detection: add
╌╌╌as an alternative to───infindGreaterThanMessageBox/findGenericSlimMessageBox. - 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.