classifyMissingRun disambiguates the sql.ErrNoRows returned by InsertChatDebugStep's locked_run CTE. The CTE filters on id, chat_id, and finished_at IS NULL, so empty RETURNING rows can mean the run is absent, belongs to a different chat, or has already been finalized. GetChatDebugRunByID is keyed o
( ctx context.Context, params CreateStepParams, )
| 438 | // finalized. GetChatDebugRunByID is keyed only by id, which is |
| 439 | // sufficient to tell these cases apart. |
| 440 | func (s *Service) classifyMissingRun( |
| 441 | ctx context.Context, |
| 442 | params CreateStepParams, |
| 443 | ) error { |
| 444 | run, err := s.db.GetChatDebugRunByID(chatdContext(ctx), params.RunID) |
| 445 | if errors.Is(err, sql.ErrNoRows) { |
| 446 | return errRunNotFound |
| 447 | } |
| 448 | if err != nil { |
| 449 | return xerrors.Errorf("look up parent run after failed step insert: %w", err) |
| 450 | } |
| 451 | if run.ChatID != params.ChatID { |
| 452 | return errRunNotFound |
| 453 | } |
| 454 | if run.FinishedAt.Valid { |
| 455 | return errRunFinalized |
| 456 | } |
| 457 | // The run matches the caller's (run_id, chat_id) and is still |
| 458 | // open, yet the INSERT returned no rows. This is unexpected |
| 459 | // under write-once-finalize semantics and likely indicates a |
| 460 | // concurrent delete or unrelated defect; surface it instead of |
| 461 | // silently masking it as a terminal case. |
| 462 | return xerrors.Errorf( |
| 463 | "InsertChatDebugStep returned no rows but run is still active (run_id=%s)", |
| 464 | params.RunID, |
| 465 | ) |
| 466 | } |
| 467 | |
| 468 | // UpdateStep updates an existing debug step and emits a step update event. |
| 469 | // When a terminal status is set without an explicit FinishedAt, the |
no test coverage detected