[PIX-13312] Возвращать 409 вместо 500 при повторном чекауте тем же executionRunId #219

Merged
andrei merged 1 commit from agent/fullstack/pix-13312 into master 2026-07-07 16:37:45 +00:00
Owner

Что сделано

checkout() в server/src/services/issues.ts теперь ловит нарушение уникального индекса issues_active_execution_run_uq (один активный executionRunId — одна активная задача) и превращает его в понятный 409 Conflict с телом {"error": "execution_run_already_active", "activeIssueId": "..."} вместо сырого Postgres-исключения, которое раньше утекало наверх как необработанный 500.

Drizzle оборачивает ошибку postgres в DrizzleQueryError с .cause, поэтому проверка isActiveExecutionRunConflict() смотрит и на верхний объект ошибки, и на error.cause (там реальный код 23505 и constraint_name).

Зачем

Когда один и тот же прогон агента (executionRunId), уже владеющий одной активной задачей, пытается зачекаутить вторую задачу (например, потрогать смежную задачу в рамках того же heartbeat), запрос падал сырым 500 вместо ожидаемого 409. Это замусоривало боевые логи стектрейсами на штатном сценарии и не давало агентам отличить "issue занята другим агентом" (обычный 409) от "нельзя чекаутить вторую задачу из этого же run".

План тестирования

  • Добавлен регрессионный тест в server/src/__tests__/issues-service.test.ts (returns 409 instead of a raw 500 when the same executionRunId tries to check out a second issue) на embedded Postgres: чекаутит issue A текущим runId, затем чекаутит issue B тем же runId и ожидает 409 с activeIssueId issue A вместо необработанного исключения; проверяет, что issue B осталась нетронутой (todo, без checkoutRunId/executionRunId).
  • pnpm exec vitest run src/__tests__/issues-service.test.ts — 99/100 зелёных (единственное падение selects primary workspace when multiple workspaces exist and one is primary — предсуществующее, не связано с этим изменением, воспроизводится и на чистом master).
  • pnpm exec vitest run src/__tests__/heartbeat-execution-run-one-to-one-linkage.test.ts (тесты PIX-13106 для самого констрейнта) — все 3 зелёные, констрейнт продолжает работать как раньше.
  • pnpm typecheck по всему монорепо прошёл в рамках pre-push хука перед пушем.

Где могу ошибаться

  • Обрабатываю только конкретно issues_active_execution_run_uq; если у Postgres-драйвера в другой версии/конфигурации имя поля будет не constraint_name, а что-то ещё — проверка не сработает и упадёт обратно в общий throw error, то есть в худшем случае вернётся прежнее поведение (500), а не хуже.
  • activeIssueId в теле 409 ищется отдельным SELECT после перехвата ошибки — теоретически между UPDATE и этим SELECT активная задача могла уже освободиться, тогда activeIssueId будет null; это не ломает контракт (409 всё равно возвращается), просто поле будет пустым в редком гоночном случае.
## Что сделано `checkout()` в `server/src/services/issues.ts` теперь ловит нарушение уникального индекса `issues_active_execution_run_uq` (один активный executionRunId — одна активная задача) и превращает его в понятный `409 Conflict` с телом `{"error": "execution_run_already_active", "activeIssueId": "..."}` вместо сырого Postgres-исключения, которое раньше утекало наверх как необработанный `500`. Drizzle оборачивает ошибку postgres в `DrizzleQueryError` с `.cause`, поэтому проверка `isActiveExecutionRunConflict()` смотрит и на верхний объект ошибки, и на `error.cause` (там реальный код `23505` и `constraint_name`). ## Зачем Когда один и тот же прогон агента (`executionRunId`), уже владеющий одной активной задачей, пытается зачекаутить вторую задачу (например, потрогать смежную задачу в рамках того же heartbeat), запрос падал сырым `500` вместо ожидаемого `409`. Это замусоривало боевые логи стектрейсами на штатном сценарии и не давало агентам отличить "issue занята другим агентом" (обычный 409) от "нельзя чекаутить вторую задачу из этого же run". ## План тестирования - Добавлен регрессионный тест в `server/src/__tests__/issues-service.test.ts` (`returns 409 instead of a raw 500 when the same executionRunId tries to check out a second issue`) на embedded Postgres: чекаутит issue A текущим runId, затем чекаутит issue B тем же runId и ожидает `409` с `activeIssueId` issue A вместо необработанного исключения; проверяет, что issue B осталась нетронутой (`todo`, без `checkoutRunId`/`executionRunId`). - `pnpm exec vitest run src/__tests__/issues-service.test.ts` — 99/100 зелёных (единственное падение `selects primary workspace when multiple workspaces exist and one is primary` — предсуществующее, не связано с этим изменением, воспроизводится и на чистом `master`). - `pnpm exec vitest run src/__tests__/heartbeat-execution-run-one-to-one-linkage.test.ts` (тесты PIX-13106 для самого констрейнта) — все 3 зелёные, констрейнт продолжает работать как раньше. - `pnpm typecheck` по всему монорепо прошёл в рамках pre-push хука перед пушем. ## Где могу ошибаться - Обрабатываю только конкретно `issues_active_execution_run_uq`; если у Postgres-драйвера в другой версии/конфигурации имя поля будет не `constraint_name`, а что-то ещё — проверка не сработает и упадёт обратно в общий `throw error`, то есть в худшем случае вернётся прежнее поведение (500), а не хуже. - `activeIssueId` в теле 409 ищется отдельным `SELECT` после перехвата ошибки — теоретически между `UPDATE` и этим `SELECT` активная задача могла уже освободиться, тогда `activeIssueId` будет `null`; это не ломает контракт (409 всё равно возвращается), просто поле будет пустым в редком гоночном случае.
fix(PIX-13312): return 409 instead of 500 on duplicate active execution run checkout
All checks were successful
security/pr-scan No security concerns detected
PR Quality Gates / PR Quality Gates (pull_request_target) Successful in 5s
Agents CI / Typecheck and Build (pull_request) Successful in 6m9s
Agents CI / API Tests (pull_request) Successful in 17m24s
27ac59c61e
checkout() let the issues_active_execution_run_uq unique-violation escape as
a raw Postgres error when the same executionRunId tried to check out a second
issue while already holding one active issue. Catch that specific constraint
violation (top-level or wrapped under DrizzleQueryError.cause) and translate
it into a 409 with { error: "execution_run_already_active", activeIssueId }
so callers can distinguish it from an ordinary "issue taken by another agent"
409 without a Postgres stack trace hitting the logs.
andrei merged commit 2b814909db into master 2026-07-07 16:37:45 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
europa-tech-srl/europa-tech-agents!219
No description provided.