mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(chatd): persist last_error on chats table (#22436)
Adds a nullable `last_error` column to the `chats` table so error reasons survive page reloads. **Backend:** - Migration adds `last_error TEXT` (nullable) to chats - `UpdateChatStatus` writes the error reason when status transitions to `error`, clears it (NULL) on recovery - `convertChat` maps `sql.NullString` to `*string` in the SDK **Frontend:** - Sidebar falls back to `chat.last_error` when no stream error reason is cached - Chat detail page does the same for `persistedErrorReason` - Fixtures updated for new required field
This commit is contained in:
+12
-3
@@ -478,6 +478,7 @@ func (p *Server) EditMessage(
|
||||
WorkerID: uuid.NullUUID{},
|
||||
StartedAt: sql.NullTime{},
|
||||
HeartbeatAt: sql.NullTime{},
|
||||
LastError: sql.NullString{},
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("set chat pending: %w", err)
|
||||
@@ -739,6 +740,7 @@ func setChatPendingWithStore(
|
||||
WorkerID: uuid.NullUUID{},
|
||||
StartedAt: sql.NullTime{},
|
||||
HeartbeatAt: sql.NullTime{},
|
||||
LastError: sql.NullString{},
|
||||
})
|
||||
if err != nil {
|
||||
return database.Chat{}, xerrors.Errorf("set chat pending: %w", err)
|
||||
@@ -753,6 +755,7 @@ func (p *Server) setChatWaiting(ctx context.Context, chatID uuid.UUID) (database
|
||||
WorkerID: uuid.NullUUID{},
|
||||
StartedAt: sql.NullTime{},
|
||||
HeartbeatAt: sql.NullTime{},
|
||||
LastError: sql.NullString{},
|
||||
})
|
||||
if err != nil {
|
||||
return database.Chat{}, err
|
||||
@@ -810,6 +813,7 @@ func insertUserMessageAndSetPending(
|
||||
WorkerID: uuid.NullUUID{},
|
||||
StartedAt: sql.NullTime{},
|
||||
HeartbeatAt: sql.NullTime{},
|
||||
LastError: sql.NullString{},
|
||||
})
|
||||
if err != nil {
|
||||
return database.ChatMessage{}, database.Chat{}, xerrors.Errorf("set chat pending: %w", err)
|
||||
@@ -1622,8 +1626,9 @@ func (p *Server) processChat(ctx context.Context, chat database.Chat) {
|
||||
Valid: true,
|
||||
})
|
||||
|
||||
// Determine the final status to set when we're done.
|
||||
// Determine the final status and last error to set when we're done.
|
||||
status := database.ChatStatusWaiting
|
||||
lastError := ""
|
||||
remainingQueuedMessages := []database.ChatQueuedMessage{}
|
||||
shouldPublishQueueUpdate := false
|
||||
|
||||
@@ -1636,7 +1641,8 @@ func (p *Server) processChat(ctx context.Context, chat database.Chat) {
|
||||
// Handle panics gracefully.
|
||||
if r := recover(); r != nil {
|
||||
logger.Error(cleanupCtx, "panic during chat processing", slog.F("panic", r))
|
||||
p.publishError(chat.ID, panicFailureReason(r))
|
||||
lastError = panicFailureReason(r)
|
||||
p.publishError(chat.ID, lastError)
|
||||
status = database.ChatStatusError
|
||||
}
|
||||
|
||||
@@ -1707,6 +1713,7 @@ func (p *Server) processChat(ctx context.Context, chat database.Chat) {
|
||||
WorkerID: uuid.NullUUID{},
|
||||
StartedAt: sql.NullTime{},
|
||||
HeartbeatAt: sql.NullTime{},
|
||||
LastError: sql.NullString{String: lastError, Valid: lastError != ""},
|
||||
})
|
||||
return updateErr
|
||||
}, nil)
|
||||
@@ -1746,7 +1753,8 @@ func (p *Server) processChat(ctx context.Context, chat database.Chat) {
|
||||
}
|
||||
logger.Error(ctx, "failed to process chat", slog.Error(err))
|
||||
if reason, ok := processingFailureReason(err); ok {
|
||||
p.publishError(chat.ID, reason)
|
||||
lastError = reason
|
||||
p.publishError(chat.ID, lastError)
|
||||
}
|
||||
status = database.ChatStatusError
|
||||
return
|
||||
@@ -2458,6 +2466,7 @@ func (p *Server) recoverStaleChats(ctx context.Context) {
|
||||
WorkerID: uuid.NullUUID{},
|
||||
StartedAt: sql.NullTime{},
|
||||
HeartbeatAt: sql.NullTime{},
|
||||
LastError: sql.NullString{},
|
||||
})
|
||||
if err != nil {
|
||||
p.logger.Error(ctx, "failed to recover stale chat",
|
||||
|
||||
@@ -577,6 +577,61 @@ func TestWaitingChatsAreNotRecoveredAsStale(t *testing.T) {
|
||||
"waiting chat should not be modified by stale recovery")
|
||||
}
|
||||
|
||||
func TestUpdateChatStatusPersistsLastError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, ps := dbtestutil.NewDB(t)
|
||||
_ = newTestServer(t, db, ps, uuid.New())
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
user, model := seedChatDependencies(ctx, t, db)
|
||||
|
||||
chat, err := db.InsertChat(ctx, database.InsertChatParams{
|
||||
OwnerID: user.ID,
|
||||
Title: "error-persisted",
|
||||
LastModelConfigID: model.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Simulate a chat that failed with an error.
|
||||
errorMessage := "stream response: status 500: internal server error"
|
||||
chat, err = db.UpdateChatStatus(ctx, database.UpdateChatStatusParams{
|
||||
ID: chat.ID,
|
||||
Status: database.ChatStatusError,
|
||||
WorkerID: uuid.NullUUID{},
|
||||
StartedAt: sql.NullTime{},
|
||||
HeartbeatAt: sql.NullTime{},
|
||||
LastError: sql.NullString{String: errorMessage, Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, database.ChatStatusError, chat.Status)
|
||||
require.Equal(t, sql.NullString{String: errorMessage, Valid: true}, chat.LastError)
|
||||
|
||||
// Verify the error is persisted when re-read from the database.
|
||||
fromDB, err := db.GetChatByID(ctx, chat.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, database.ChatStatusError, fromDB.Status)
|
||||
require.Equal(t, sql.NullString{String: errorMessage, Valid: true}, fromDB.LastError)
|
||||
|
||||
// Verify the error is cleared when the chat transitions to a
|
||||
// non-error status (e.g. pending after a retry).
|
||||
chat, err = db.UpdateChatStatus(ctx, database.UpdateChatStatusParams{
|
||||
ID: chat.ID,
|
||||
Status: database.ChatStatusPending,
|
||||
WorkerID: uuid.NullUUID{},
|
||||
StartedAt: sql.NullTime{},
|
||||
HeartbeatAt: sql.NullTime{},
|
||||
LastError: sql.NullString{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, database.ChatStatusPending, chat.Status)
|
||||
require.False(t, chat.LastError.Valid)
|
||||
|
||||
fromDB, err = db.GetChatByID(ctx, chat.ID)
|
||||
require.NoError(t, err)
|
||||
require.False(t, fromDB.LastError.Valid)
|
||||
}
|
||||
|
||||
func newTestServer(
|
||||
t *testing.T,
|
||||
db database.Store,
|
||||
|
||||
Reference in New Issue
Block a user