[PIX-13257] Scheduler повторно проверяет terminal status issue перед стартом adapter #211

Merged
andrei merged 5 commits from agent/cto/pix-13257 into master 2026-07-07 10:35:04 +00:00
Owner

Что сделано

Scheduler теперь повторно проверяет terminal status целевого issue прямо перед стартом adapter/process, а не только один раз при claim очереди.

  • Расширен уже существующий чекпоинт shouldStopCancelledExecution() (вызывается перед resolve runtime-skills, перед environment lease и перед стартом adapter) новой функцией cancelRunIfIssueReachedTerminalStatus().
  • Если issue стал done/cancelled уже ПОСЛЕ claim run'а (например, workspace/environment realization заняли несколько секунд, и issue за это время закрыл другой run), run отменяется с errorCode: issue_terminal_status, execution-lock issue освобождается, отложенные wake промоутятся — по аналогии с уже существующей pre-claim проверкой в evaluateQueuedRunStaleness().
  • Явный resume (resumeIntent/followUpRequested в contextSnapshot) по-прежнему разрешён — не блокируется.

Зачем

CEO heartbeat 2026-07-07 обнаружил live run по уже done issue (PIX-13247): run стартовал через 14 секунд ПОСЛЕ того как issue стал done. Hermes вручную остановил процесс. Корневая причина: между claim (запись run.status=running) и фактическим вызовом adapter.execute() проходит workspace/environment realization (может занимать секунды из-за git worktree + embedded postgres), и в это окно issue мог уже закрыться другим run'ом — но ничего это не перепроверяло.

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

  • Новый regression-тест server/src/__tests__/heartbeat-issue-terminal-status-before-adapter-start.test.ts: мокает realizeExecutionWorkspace управляемой паузой, переводит issue в done пока run "застрял" внутри workspace realization, снимает паузу и проверяет что run отменяется с issue_terminal_status, а adapter НИКОГДА не вызывается. Второй тест подтверждает что explicit resume (resumeIntent: true) всё ещё нормально доходит до adapter. Оба теста зелёные.
  • pnpm run typecheck (полный, все пакеты) — 0 ошибок.
  • pnpm exec vitest run server/src/__tests__/heartbeat-stale-queue-invalidation.test.ts — 12/14 passed; 2 падения предсуществующие и НЕ связаны с этим PR (подтверждено через git stash bisect моего diff — падают идентично с фиксом и без него). Заведена отдельная задача PIX-13260 на эти 2 теста с root-cause анализом.
  • Дополнительно два committed фикса на pre-push hooks (эта ветка была создана от коммита, который на 658 коммитов отстаёт от master и не содержал двух более новых pre-push проверок): восстановлен scripts/check-not-shared-root-push.mjs (PIX-12727, отсутствовал в package.json этой ветки) и помечена как allowed одна ложно-положительная строка в git-delivery-gate.ts (человекочитаемый текст инструкции внутри error message, не реальный вызов git push).

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

  • Есть более узкое остаточное окно между чекпоинтом before_adapter_start и фактическим вызовом adapter.execute() (ensureRuntimeServicesForRun + несколько DB-записей) — не перекрыто новой проверкой, т.к. потребовало бы отдельной cleanup-логики для уже запущенных runtime-сервисов. Основной инцидент (14-секундная задержка) приходится на workspace realization, которая перекрывается новой проверкой полностью.
  • Ветка сильно отстаёт от master (658 коммитов) — ребейз не делал (слишком рискованно/дорого для этого фикса), полагаюсь на чистый merge через PR.
## Что сделано Scheduler теперь повторно проверяет terminal status целевого issue прямо перед стартом adapter/process, а не только один раз при claim очереди. - Расширен уже существующий чекпоинт `shouldStopCancelledExecution()` (вызывается перед resolve runtime-skills, перед environment lease и перед стартом adapter) новой функцией `cancelRunIfIssueReachedTerminalStatus()`. - Если issue стал `done`/`cancelled` уже ПОСЛЕ claim run'а (например, workspace/environment realization заняли несколько секунд, и issue за это время закрыл другой run), run отменяется с `errorCode: issue_terminal_status`, execution-lock issue освобождается, отложенные wake промоутятся — по аналогии с уже существующей pre-claim проверкой в `evaluateQueuedRunStaleness()`. - Явный resume (`resumeIntent`/`followUpRequested` в contextSnapshot) по-прежнему разрешён — не блокируется. ## Зачем CEO heartbeat 2026-07-07 обнаружил live run по уже done issue (PIX-13247): run стартовал через 14 секунд ПОСЛЕ того как issue стал done. Hermes вручную остановил процесс. Корневая причина: между claim (запись run.status=running) и фактическим вызовом adapter.execute() проходит workspace/environment realization (может занимать секунды из-за git worktree + embedded postgres), и в это окно issue мог уже закрыться другим run'ом — но ничего это не перепроверяло. ## План тестирования - Новый regression-тест `server/src/__tests__/heartbeat-issue-terminal-status-before-adapter-start.test.ts`: мокает `realizeExecutionWorkspace` управляемой паузой, переводит issue в `done` пока run "застрял" внутри workspace realization, снимает паузу и проверяет что run отменяется с `issue_terminal_status`, а adapter НИКОГДА не вызывается. Второй тест подтверждает что explicit resume (`resumeIntent: true`) всё ещё нормально доходит до adapter. Оба теста зелёные. - `pnpm run typecheck` (полный, все пакеты) — 0 ошибок. - `pnpm exec vitest run server/src/__tests__/heartbeat-stale-queue-invalidation.test.ts` — 12/14 passed; 2 падения предсуществующие и НЕ связаны с этим PR (подтверждено через `git stash` bisect моего diff — падают идентично с фиксом и без него). Заведена отдельная задача PIX-13260 на эти 2 теста с root-cause анализом. - Дополнительно два committed фикса на pre-push hooks (эта ветка была создана от коммита, который на 658 коммитов отстаёт от master и не содержал двух более новых pre-push проверок): восстановлен `scripts/check-not-shared-root-push.mjs` (PIX-12727, отсутствовал в package.json этой ветки) и помечена как allowed одна ложно-положительная строка в `git-delivery-gate.ts` (человекочитаемый текст инструкции внутри error message, не реальный вызов git push). ## Где могу ошибаться - Есть более узкое остаточное окно между чекпоинтом `before_adapter_start` и фактическим вызовом `adapter.execute()` (ensureRuntimeServicesForRun + несколько DB-записей) — не перекрыто новой проверкой, т.к. потребовало бы отдельной cleanup-логики для уже запущенных runtime-сервисов. Основной инцидент (14-секундная задержка) приходится на workspace realization, которая перекрывается новой проверкой полностью. - Ветка сильно отстаёт от master (658 коммитов) — ребейз не делал (слишком рискованно/дорого для этого фикса), полагаюсь на чистый merge через PR.
Scheduler could still spawn an adapter process for an issue that already
reached done/cancelled: claimQueuedRun() only checked the issue's terminal
status once, before marking the run "running". Workspace/environment
realization between the claim and the actual adapter invocation can take
many seconds, during which the issue can independently finish (e.g. another
run for the same issue completes first). Nothing re-validated the issue's
status in that window, so a stale claimed run could still invoke the
adapter and spawn an OS process for an already-closed issue (PIX-13257).

Extend the existing shouldStopCancelledExecution() checkpoint (already
called before runtime skills resolution, before environment leasing, and
before the adapter starts) to also re-check the target issue's status via
a new cancelRunIfIssueReachedTerminalStatus() helper. If the issue reached
done/cancelled since the claim (and the run isn't an explicit resume),
cancel with errorCode issue_terminal_status, release the issue execution
lock, and promote any deferred wakes -- mirroring the existing pre-claim
staleness path -- instead of starting the adapter.

Adds a regression test that pauses workspace realization, flips the issue
to done mid-flight, and asserts the run is cancelled with
issue_terminal_status and the adapter mock is never invoked, plus a test
confirming explicit resume wakes (resumeIntent) still run normally.
This branch's base commit predates PIX-12727's shared-root push guard, but
core.hooksPath is an absolute path shared by every worktree of this repo, so
the pre-push hook already installed there calls
"pnpm run check:not-shared-root-push" regardless of which branch/commit a
worktree happens to be on. Without this script, the hook fails with
ERR_PNPM_NO_SCRIPT and blocks every push from this branch.

Port the script, its test, and the package.json entries verbatim from
origin/master so the shared hook resolves correctly again.
chore(hooks): mark git-delivery-gate instruction text as allowed git-push mention
All checks were successful
security/pr-scan No security concerns detected
PR Quality Gates / PR Quality Gates (pull_request_target) Successful in 6s
Agents CI / Typecheck and Build (pull_request) Successful in 5m21s
6a054d70b2
check-no-git-push.mjs (adapter/runtime code must never call git push itself)
flagged this file's human-readable remediation string ("1. git push origin
<branch>...") surfaced inside an error message for an operator/agent to run
manually. It is not an actual git push invocation, so annotate it with the
script's own documented paperclip:allow-git-push opt-in instead of leaving
every push from this branch blocked by a false positive.
merge: sync agent/cto/pix-13257 with origin/master
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 6m5s
Agents CI / API Tests (pull_request) Successful in 17m22s
82dcc6793b
Branch was 658 commits behind master. Resolve the 3 real conflicts:

- package.json: keep both new script entries (mine + master's
  test:hermes-gateway-smoke).
- server/src/services/git-delivery-gate.ts: take master's version, it
  already carries the same false-positive-comment fix I made
  independently.
- server/src/services/heartbeat.ts: combine my cancelRunIfIssueReachedTerminalStatus
  re-check (passing issueId into shouldStopCancelledExecution at the
  before_environment_lease/before_adapter_start checkpoints) with
  master's newer recordWorkspaceFinalizeForAbortedExecution barrier
  (PIX-13244), so an issue-terminal-status cancellation still records
  the workspace_finalize row the same way any other early-return does.

Verified after merge: pnpm --filter @paperclipai/server typecheck passes,
and heartbeat-issue-terminal-status-before-adapter-start.test.ts (the
PIX-13257 regression test) passes 2/2. heartbeat-stale-queue-invalidation.test.ts
fails identically on a clean origin/master checkout (verified in an
isolated worktree), so its failures are pre-existing flakiness, not a
regression from this merge.
fix(tests): update static regression assertions after terminal-status re-check
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 6m23s
Agents CI / API Tests (pull_request) Successful in 17m28s
62e4ec607f
337d252fa added an issueId argument to shouldStopCancelledExecution() at
the before_environment_lease and before_adapter_start checkpoints
(PIX-13257). heartbeat-static-regression.test.ts asserted the exact old
call-site text without the new argument, so both checks now fail against
current source.

Also fixes an unrelated, pre-existing drift in the same file: the
"promotes due scheduled retries" test still expected the literal
`promoteDueScheduledRetries(new Date())` call, but resumeQueuedRuns()
already reuses its local `now` variable for that call (confirmed present
verbatim in origin/master, unaffected by PIX-13257). Bundled here since it
sits in the same file/test run and was already red before this change.

Verified server/src/__tests__/heartbeat-process-recovery.test.ts's 82/95
failures are a separate, pre-existing issue unrelated to this branch
(identical failure count reproduces against pristine origin/master
heartbeat.ts) -- filed separately, not touched here.
Author
Owner

Доп. коммит 62e4ec607: чиню heartbeat-static-regression.test.ts — 337d252fa добавил аргумент issueId в вызовы shouldStopCancelledExecution("before_adapter_start"/"before_environment_lease"), а этот тест проверял точный текст вызова без нового аргумента (2 из 3 упавших ассертов). Заодно поправил 1 несвязанный pre-existing разъезд в том же файле (promoteDueScheduledRetries(now) vs ожидаемый new Date()) — подтверждено идентично на чистом origin/master, не моя регрессия.

Локально зелено: heartbeat-issue-terminal-status-before-adapter-start.test.ts 2/2, heartbeat-static-regression.test.ts 81/81, pnpm exec tsc --noEmit — 0 ошибок. CI на раннерах сейчас подвисает в очереди (waiting/blocked) по всему репозиторию, не специфично для этого PR — подтверждено через forgejo_actions_queue_sweeper.py (0 orphaned/stale кандидатов). Смержу как только CI дойдёт до success.

Доп. коммит 62e4ec607: чиню heartbeat-static-regression.test.ts — 337d252fa добавил аргумент issueId в вызовы shouldStopCancelledExecution("before_adapter_start"/"before_environment_lease"), а этот тест проверял точный текст вызова без нового аргумента (2 из 3 упавших ассертов). Заодно поправил 1 несвязанный pre-existing разъезд в том же файле (promoteDueScheduledRetries(now) vs ожидаемый new Date()) — подтверждено идентично на чистом origin/master, не моя регрессия. Локально зелено: heartbeat-issue-terminal-status-before-adapter-start.test.ts 2/2, heartbeat-static-regression.test.ts 81/81, pnpm exec tsc --noEmit — 0 ошибок. CI на раннерах сейчас подвисает в очереди (waiting/blocked) по всему репозиторию, не специфично для этого PR — подтверждено через forgejo_actions_queue_sweeper.py (0 orphaned/stale кандидатов). Смержу как только CI дойдёт до success.
andrei merged commit 671b952776 into master 2026-07-07 10:35:04 +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!211
No description provided.