UpdateRun updates an existing debug run and emits a run update event. When a terminal status is set without an explicit FinishedAt, the service auto-fills the timestamp so the row is immediately visible to the InsertChatDebugStep atomic guard (finished_at IS NULL). UpdateChatDebugRun itself enforces
( ctx context.Context, params UpdateRunParams, )
| 310 | // never overwrite the original completion timestamp, so calling this |
| 311 | // more than once on an already-finalized run is idempotent. |
| 312 | func (s *Service) UpdateRun( |
| 313 | ctx context.Context, |
| 314 | params UpdateRunParams, |
| 315 | ) (database.ChatDebugRun, error) { |
| 316 | if params.Status.IsTerminal() && params.FinishedAt.IsZero() { |
| 317 | params.FinishedAt = s.clock.Now() |
| 318 | } |
| 319 | run, err := s.db.UpdateChatDebugRun(chatdContext(ctx), |
| 320 | database.UpdateChatDebugRunParams{ |
| 321 | RootChatID: uuid.NullUUID{}, |
| 322 | ParentChatID: uuid.NullUUID{}, |
| 323 | ModelConfigID: uuid.NullUUID{}, |
| 324 | TriggerMessageID: sql.NullInt64{}, |
| 325 | HistoryTipMessageID: sql.NullInt64{}, |
| 326 | Status: nullString(string(params.Status)), |
| 327 | Provider: sql.NullString{}, |
| 328 | Model: sql.NullString{}, |
| 329 | Summary: s.nullJSON(ctx, params.Summary), |
| 330 | FinishedAt: nullTime(params.FinishedAt), |
| 331 | Now: s.clock.Now(), |
| 332 | ID: params.ID, |
| 333 | ChatID: params.ChatID, |
| 334 | }) |
| 335 | if err != nil { |
| 336 | return database.ChatDebugRun{}, err |
| 337 | } |
| 338 | |
| 339 | s.publishEvent(ctx, run.ChatID, EventKindRunUpdate, run.ID, uuid.Nil) |
| 340 | return run, nil |
| 341 | } |
| 342 | |
| 343 | // errRunFinalized is returned by CreateStep when the parent run has |
| 344 | // already reached a terminal state (finished_at IS NOT NULL). This |