Conversation
|
✅ Preview binaries are ready! To test with modules: |
mafredri
left a comment
There was a problem hiding this comment.
Nice work so far! I left a few comments and suggestions. Mainly about moving some concerns from httpapi to cmd/server. I think it would be helpful to have some tests based on real agent session restoration output to evaluate the screentracker changes.
| func (s *Server) HandleSignals(ctx context.Context, process *termexec.Process) { | ||
| // Handle shutdown signals (SIGTERM, SIGINT only on Windows) | ||
| shutdownCh := make(chan os.Signal, 1) | ||
| signal.Notify(shutdownCh, os.Interrupt, syscall.SIGTERM) |
There was a problem hiding this comment.
Does this compile on Windows? IIRC we can only support os.Interrupt there.
There was a problem hiding this comment.
compiles - yes (since PR Preview Build / Build Release Binaries (pull_request) passes), but haven't tested it.
# Conflicts: # cmd/server/server.go # lib/httpapi/server.go # lib/screentracker/conversation.go # lib/screentracker/pty_conversation.go # lib/screentracker/pty_conversation_test.go
# Conflicts: # lib/httpapi/server.go # lib/screentracker/pty_conversation.go
mafredri
left a comment
There was a problem hiding this comment.
Test coverage seems good, but I would really like to see real testdata created for e.g. Claude where all output from the initial conversation, and then again for the restoration part. And testing that AgentAPI does in fact handle this correctly.
I'm not sure how feasible it is, but ideally it'd be nice to capture everything, including control characters, that Claude outputs. Think asciinema recording.
Also, I think we need to really consider everything that can affect the AI agent output. For instance, given the nature of adjustScreenAfterStateLoad, what if --term-height and --term-width have been adjusted between invocations? If the state is being restored, we should probably do so much earlier (in runServer) and print warnings that these options are being overridden by the state restoration and forcing the previous options. Wdyt?
| // Store the first stable snapshot for filtering later | ||
| snapshots := c.snapshotBuffer.GetAll() | ||
| if len(snapshots) > 0 { | ||
| c.firstStableSnapshot = c.cfg.FormatMessage(strings.TrimSpace(snapshots[len(snapshots)-1].screen), "") |
There was a problem hiding this comment.
I suppose we need a FormatMessage nil check here?
Also, what if FormatMessage function has changed between session save and restore (different AgentAPI versions). Potential issue?
There was a problem hiding this comment.
Also, what if FormatMessage function has changed between session save and restore (different AgentAPI versions). Potential issue?
This doesn't pose an issue, c.firstStableSnapshot is not stored in the state, it is used to strip the conversation history loaded by the agent in case of --continue/--resume so that we can load our saved state messages.
Agreed
imo, this is not an issue; nothing in adjustScreenAfterStateLoad will be affected by height/width. Its only role rn is to strip the conversation history loaded by the agent when using --continue/--resume. |
So we don't need to think about how a terminal size affects line-splitting/ASCII formatting? |
|
|
||
| if c.initialPromptReady && !c.loadStateSuccessful && c.cfg.StatePersistenceConfig.LoadState { | ||
| _ = c.loadState() | ||
| c.loadStateSuccessful = true |
There was a problem hiding this comment.
Outright failure will cause the session to be essentially "bricked" until you fix, rename, or move the state file. We should definitely surface the error, but complete failure is not ideal.
Deep Code ReviewOverall risk: Medium. The state file format decisions are one-way doors, and there are a few logic bugs that could produce incorrect behavior in edge cases. The architecture is sound and the reviewer feedback from mafredri and johnstcn has been largely addressed (PID file moved to cmd/server, signal handlers split per-platform, P2-1:
|
| // Stop the HTTP server | ||
| shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
| defer cancel() | ||
| if err := srv.Stop(shutdownCtx); err != nil { | ||
| logger.Error("Failed to stop HTTP server", "error", err) | ||
| } |
There was a problem hiding this comment.
We could consider moving this into case <-gracefulCtx.Done(): above? I'm guessing we don't have to call srv.Stop if we receive on serverErrCh.
Does stop error if the server already closed? If yes, we'll end up printing a misleading error here.
EDIT: I looked at Stop and the once does guard against multiple Stops, but not against Stop producing an error if the server closed before?
There was a problem hiding this comment.
We return early if we receive anything(non nil) on serverErrCh
select {
case err := <-serverErrCh:
if err != nil {
return xerrors.Errorf("failed to start server: %w", err)
I looked at Stop and the once does guard against multiple Stops, but not against Stop producing an error if the server closed before?
True, I had an alternative in mind, but I decided to proceed with once here for the above-mentioned reason. I'll add a check in Stop
I'll check for this ErrServerClosed, and return nil in that case.
From docs
Once Shutdown has been called on a server, it may not be reused; future calls to methods such as Serve will return ErrServerClosed.
cmd/server/server.go
Outdated
| } | ||
|
|
||
| // Close the process | ||
| if err := process.Close(logger, 5*time.Second); err != nil { |
There was a problem hiding this comment.
What's the result of Close if the process already exited? Should we check processExitCh first?
There was a problem hiding this comment.
Moved this to the select-case block below.
select {
case err := <-processExitCh:
if err != nil {
return xerrors.Errorf("agent exited with error: %w", err)
}
default:
// Close the process
if err := process.Close(logger, 5*time.Second); err != nil {
logger.Error("Failed to close process cleanly", "error", err)
}
}
|
|
||
| // Enqueue initial prompt once after agent is ready (and after state is potentially loaded) | ||
| if c.initialPromptReady && len(c.cfg.InitialPrompt) > 0 && !c.initialPromptSent { | ||
| c.outboundQueue <- outboundMessage{parts: c.cfg.InitialPrompt, errCh: nil} |
There was a problem hiding this comment.
Does this play nicely with the logic around userSentMessageAfterLoadState? If possible, I'd like to see that state flag removed as it adds a bit of confusion about how the whole pipeline interacts.
The scenario I have in mind: Resume with a new "initial prompt", what happens? Will the new prompt be sent? Will it bypass userSentMessageAfterLoadStat?
There was a problem hiding this comment.
Resume with a new "initial prompt", what happens? Will the new prompt be sent? Will it bypass userSentMessageAfterLoadStat
- Yes, it will be sent (given that it's not the same as the initialPrompt in the stateFile).
- No, it won't bypass
userSentMessageAfterLoadState. TheuserSentMessageAfterLoadStateis set insendMessage, and sendMessage is called for everything that comes in viaoutboundQueuechannel (this is done in a go func inStart).
| s.logger.Error("Failed to send event", "subscriberId", subscriberId, "error", err) | ||
| return | ||
| } | ||
| case <-s.shutdownCtx.Done(): |
There was a problem hiding this comment.
Potential future enhancement, if we set the http server base context to shutdownCtx, we only need to handle the request context as it will inherit from the base context. (Needs verification if there's a benefit here though.)

Closes: coder/internal#1256
MergeAfter: #172