Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 136
fix(screentracker): make writeStabilize Phase 1 non-fatal when agents don't echo input#208
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
fcc12e426d03e9a610148f6f14b8a80af343cb783fdfd3caaFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| 1 function greet() { | ||
| 2 - console.log("Hello, World!"); | ||
| 2 + console.log("Hello, Claude!"); | ||
| 3 } | ||
| ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ | ||
| > Try "what does this code do?" | ||
| ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ | ||
| Syntax theme: Monokai Extended (ctrl+t to disable) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| ╭────────────────────────────────────────────╮ | ||
| │ ✻ Welcome to Claude Code! │ | ||
| │ │ | ||
| │ /help for help │ | ||
| ╰────────────────────────────────────────────╯ | ||
| ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ | ||
| │ Type your message... | ||
| ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -3,6 +3,7 @@ package screentracker | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "errors" | ||
| "fmt" | ||
| "log/slog" | ||
| "os" | ||
| @@ -16,6 +17,26 @@ import ( | ||
| "golang.org/x/xerrors" | ||
| ) | ||
| const ( | ||
| // writeStabilizeEchoTimeout is the timeout for the echo | ||
| // detection WaitFor loop in writeStabilize Phase 1. The | ||
| // effective ceiling may be slightly longer because the | ||
| // stability check inside the condition runs outside | ||
| // WaitFor's timeout select. Non-echoing agents (e.g. TUI | ||
| // agents using bracketed paste) will hit this timeout, | ||
| // which is non-fatal. | ||
| // | ||
| // TODO: move to PTYConversationConfig if agents need | ||
| // different echo detection windows. | ||
| writeStabilizeEchoTimeout = 2 * time.Second | ||
| // writeStabilizeProcessTimeout is the maximum time to wait | ||
| // for the screen to change after sending a carriage return. | ||
| // This detects whether the agent is actually processing the | ||
| // input. | ||
| writeStabilizeProcessTimeout = 15 * time.Second | ||
| ) | ||
| // A screenSnapshot represents a snapshot of the PTY at a specific time. | ||
| type screenSnapshot struct { | ||
| timestamp time.Time | ||
| @@ -411,17 +432,30 @@ func (c *PTYConversation) sendMessage(ctx context.Context, messageParts ...Messa | ||
| return nil | ||
| } | ||
| // writeStabilize writes messageParts to the screen and waits for the screen to stabilize after the message is written. | ||
| // writeStabilize writes messageParts to the PTY and waits for | ||
| // the agent to process them. It operates in two phases: | ||
| // | ||
| // Phase 1 (echo detection): writes the message text and waits | ||
| // for the screen to change and stabilize. This detects agents | ||
| // that echo typed input. If the screen doesn't change within | ||
| // writeStabilizeEchoTimeout, this is non-fatal. Many TUI | ||
| // agents buffer bracketed-paste input without rendering it. | ||
| // | ||
| // Phase 2 (processing detection): writes a carriage return | ||
| // and waits for the screen to change, indicating the agent | ||
| // started processing. This phase is fatal on timeout: if the | ||
| // agent doesn't react to Enter, it's unresponsive. | ||
| func (c *PTYConversation) writeStabilize(ctx context.Context, messageParts ...MessagePart) error { | ||
| screenBeforeMessage := c.cfg.AgentIO.ReadScreen() | ||
| for _, part := range messageParts { | ||
| if err := part.Do(c.cfg.AgentIO); err != nil { | ||
| return xerrors.Errorf("failed to write message part: %w", err) | ||
| } | ||
| } | ||
| // wait for the screen to stabilize after the message is written | ||
| // Phase 1: wait for the screen to stabilize after the | ||
| // message is written (echo detection). | ||
| if err := util.WaitFor(ctx, util.WaitTimeout{ | ||
| Timeout: 15 * time.Second, | ||
| Timeout: writeStabilizeEchoTimeout, | ||
| MinInterval: 50 * time.Millisecond, | ||
| InitialWait: true, | ||
| Clock: c.cfg.Clock, | ||
| @@ -441,14 +475,26 @@ func (c *PTYConversation) writeStabilize(ctx context.Context, messageParts ...Me | ||
| } | ||
| return false, nil | ||
| }); err != nil { | ||
| return xerrors.Errorf("failed to wait for screen to stabilize: %w", err) | ||
| } | ||
| // wait for the screen to change after the carriage return is written | ||
| if !errors.Is(err, util.WaitTimedOut) { | ||
johnstcn marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. johnstcn marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| // Context cancellation or condition errors are fatal. | ||
| return xerrors.Errorf("failed to wait for screen to stabilize: %w", err) | ||
johnstcn marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| // Phase 1 timeout is non-fatal: the agent may not echo | ||
| // input (e.g. TUI agents buffer bracketed-paste content | ||
| // internally). Proceed to Phase 2 to send the carriage | ||
| // return. | ||
| c.cfg.Logger.Info( | ||
johnstcn marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| "echo detection timed out, sending carriage return", | ||
| "timeout", writeStabilizeEchoTimeout, | ||
| ) | ||
johnstcn marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| // Phase 2: wait for the screen to change after the | ||
| // carriage return is written (processing detection). | ||
| screenBeforeCarriageReturn := c.cfg.AgentIO.ReadScreen() | ||
johnstcn marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| lastCarriageReturnTime := time.Time{} | ||
| if err := util.WaitFor(ctx, util.WaitTimeout{ | ||
| Timeout: 15 * time.Second, | ||
| Timeout: writeStabilizeProcessTimeout, | ||
| MinInterval: 25 * time.Millisecond, | ||
| Clock: c.cfg.Clock, | ||
| }, func() (bool, error) { | ||
| @@ -527,6 +573,14 @@ func (c *PTYConversation) statusLocked() ConversationStatus { | ||
| return ConversationStatusChanging | ||
| } | ||
| // The send loop gates stableSignal on initialPromptReady. | ||
| // Report "changing" until readiness is detected so that Send() | ||
| // rejects with ErrMessageValidationChanging instead of blocking | ||
| // indefinitely on a stableSignal that will never fire. | ||
| if !c.initialPromptReady { | ||
| return ConversationStatusChanging | ||
| } | ||
| // Handle initial prompt readiness: report "changing" until the queue is drained | ||
| // to avoid the status flipping "changing" -> "stable" -> "changing" | ||
| if len(c.outboundQueue) > 0 || c.sendingMessage { | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.