Skip to content

fix: honor execution activity in workflow stale reaper - #5

Closed
ddbaron wants to merge 3 commits into
mainfrom
fm/af-workflow-clock-fix-r1
Closed

fix: honor execution activity in workflow stale reaper#5
ddbaron wants to merge 3 commits into
mainfrom
fm/af-workflow-clock-fix-r1

Conversation

@ddbaron

@ddbaron ddbaron commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Summary

  • treat a workflow execution as stale only when both the workflow clock and its active paired execution clock exceed the stale fuse
  • let executions-only heartbeat/note activity protect the workflow row without adding write load
  • keep terminal legacy rows from shielding an active workflow row after the legacy reaper runs, and preserve terminal sync behavior
  • add storage regressions for heartbeat activity, silent paired rows, and legacy sync

Problem

The control plane maintains two activity clocks: executions and workflow_executions. Heartbeat/status writes update only executions.updated_at, while the workflow stale-sweeper read only workflow_executions.updated_at. Productively active leaf executions could therefore be reaped on a frozen clock: four sampled coder executions were reaped 10–13 minutes after start, matching the 10-minute fuse, despite heartbeat notes arriving every 90 seconds and file commits continuing afterward. Late completions then hit terminal records and returned HTTP 409s.

Solution

This implements the read-path fix: workflow reaping considers the newer of the workflow clock and the active paired execution clock, so both clocks must exceed the fuse before reaping. Terminal legacy rows are not treated as active activity, allowing the ordered cleanup passes to finish synchronizing both tables. This adds no heartbeat write load and makes activity in the execution record visible to the workflow reaper. It deliberately does not extend the timeout or change the SDK, SWE-AF node, or delivery semantics.

Validation

  • cd control-plane && go test ./internal/storage -count=1
  • cd control-plane && go test ./internal/storage ./internal/handlers -count=1
  • cd control-plane && go vet ./...
  • gofmt and git diff --check

The repository make lint command was attempted, but golangci-lint and ruff are not installed in this environment. The full control-plane suite was also attempted; it has unrelated failures in unmodified package/server tests, while the affected storage and handler packages pass.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

📊 Coverage gate

Thresholds from .coverage-gate.toml: per-surface ≥ 84%, aggregate ≥ 85%, max per-surface regression ≤ 1.0 pp, max aggregate regression ≤ 0.50 pp.

Surface Current Baseline Δ
control-plane 87.80% 87.40% ↑ +0.40 pp 🟡
sdk-go 93.10% 92.00% ↑ +1.10 pp 🟢
sdk-python 94.72% 93.73% ↑ +0.99 pp 🟢
sdk-typescript 91.72% 90.42% ↑ +1.30 pp 🟢
web-ui 84.77% 84.79% ↓ -0.02 pp 🟡
aggregate 85.89% 85.75% ↑ +0.14 pp 🟡

✅ Gate passed

No surface regressed past the allowed threshold and the aggregate stayed above the floor.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

📐 Patch coverage gate

Threshold: 80% on lines this PR touches vs origin/main (from .coverage-gate.toml:thresholds.min_patch).

Surface Touched lines Patch coverage Status
control-plane 63 100.00%
sdk-go 0 ➖ no changes
sdk-python 0 ➖ no changes
sdk-typescript 0 ➖ no changes
web-ui 0 ➖ no changes

✅ Patch gate passed

Every surface whose lines were touched by this PR has patch coverage at or above the threshold.

ddbaron and others added 2 commits September 9, 2026 09:16
MarkStaleWorkflowExecutions now repeats its candidate predicates in the
conditional UPDATE, but MarkStaleExecutions still only re-checked status.
A heartbeat that lands between its candidate selection and that UPDATE
therefore still flips a live execution row to timeout — the same false
timeout the workflow reaper just stopped producing, through a narrower
window (its candidate query reads the clock the heartbeat writes, so the
race is the millisecond gap between the two statements rather than the
whole run).

Give it the same treatment: the conditional UPDATE re-evaluates the
activity clock against the sweep cutoff and the non-terminal-child guard,
and the body moves behind the same post-selection seam the workflow reaper
uses so the interleaving is testable without sleeps.

Tests: a real execution-note write landing in that window leaves the row
running with its note intact; a seam that writes nothing still reaps the
silent row with the existing "no activity" message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ddbaron

ddbaron commented Sep 10, 2026

Copy link
Copy Markdown
Owner Author

Closing as redundant: this change landed upstream in Agent-Field#1046, merged as squash commit 78215f1. That merge contains both commits from this branch, plus a maintainer commit extending the same race guard to the legacy execution reaper. Nothing in this branch is unmerged, so the fork PR is no longer needed.

@ddbaron ddbaron closed this Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants