Skip to content

fix(agents): stop charging mutex queue time to the implementer's work budget - #1282

Merged
mashkovd merged 1 commit into
mainfrom
fix/implementer-deadline-counts-queue-time
Sep 19, 2026
Merged

mashkovd merged 1 commit into
mainfrom
fix/implementer-deadline-counts-queue-time

Conversation

@mashkovd

Copy link
Copy Markdown
Member

activeDeadlineSeconds sat at spec level, and a spec-level deadline runs
from submit — not from the moment the mctl-agents-proposal-claims mutex
is acquired. That mutex has capacity 1 and one proposal takes 30-70 min,
so only the first two or three submissions of a batch could start inside
the 2h budget. Everything behind them expired Pending, having never
created a pod.

The lost attempt was not the worst of it. A spec-level deadline is
workflow-wide, so once implement had consumed it, implement-fallback
and commit-and-push were both failed instantly with "Step exceeded its
deadline" — and commit-and-push is the only step that writes
.status.yaml back. The proposal stayed accepted with no branch, no PR
and no failure recorded anywhere, which is the exact state the
assert-produced truthful-status design exists to prevent.

On the night of 2026-09-19 six of nine implement runs were lost this way:
mctl-agents#333 #334, mctl-telegram#438 #569, mctl-gitops#903 #1178. The
controller log is unambiguous —

"...isn't at the front" lockType=mutex
    name=argo-workflows/Mutex/mctl-agents-proposal-claims
"Could not acquire lock named: .../mctl-agents-proposal-claims"
"Workflow pod is missing" nodePhase=Pending recentlyStarted=false

and the lock timeline matches the outcomes exactly: bcc54a61 held it
until 01:58, 0eaa9853 (telegram#438) acquired at 01:58 against its own
02:12:31 deadline, got 14 of the 40-70 min it needed, and died.

The 7200s budget moves to the run-implementer template, where a
deadline is applied to the pod and therefore runs from the pod's start,
after the mutex is acquired. It covers implement and implement-fallback
alike — both are that one template — so a primary that burns its full
budget no longer leaves the fallback nothing to run in.

Spec level keeps a ceiling, but as a runaway guard rather than a work
budget: 8h covers worst-case work (7200 + 7200 + a bounded commit) plus
room to queue behind two or three other runs. commit-and-push gets its
own 900s so that, without the tight spec ceiling, the step that records
the outcome cannot be the one that hangs.

A deep enough queue can still expire at the spec ceiling — but it now
expires having attempted nothing, rather than discarding a finished
attempt. Narrowing the mutex to the discovery+claim critical section it
actually protects is the real fix for the queue, and is tracked
separately. The precedent is in this same file: mctl-gitops-main-writes
was moved off the workflow spec onto commit-and-push for exactly this
reason, after holding it across the whole implementer run starved other
main-writers.

Verified with kubectl apply --dry-run=server.


Effect

before                                   after
spec.activeDeadlineSeconds: 7200         spec.activeDeadlineSeconds: 28800   (runaway guard)
  ├─ wait for mutex   ← charged            ├─ wait for mutex   ← not charged
  ├─ implement        ← what is left       ├─ run-implementer  activeDeadlineSeconds: 7200
  ├─ implement-fallback ← usually 0        │    implement / implement-fallback, 7200 each
  └─ commit-and-push    ← usually 0        └─ commit-and-push  activeDeadlineSeconds: 900

Not in this PR, on purpose

  • Narrowing mctl-agents-proposal-claims to discovery+claim. That is the real fix for the queue and deserves its own change, not a hotfix.
  • A semaphore of 2-3 instead of the mutex: it would weaken the claim protection the mutex exists for.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NFS84XSBZ6vMKsd7CQr6Bf

… budget

`activeDeadlineSeconds` sat at spec level, and a spec-level deadline runs
from submit — not from the moment the `mctl-agents-proposal-claims` mutex
is acquired. That mutex has capacity 1 and one proposal takes 30-70 min,
so only the first two or three submissions of a batch could start inside
the 2h budget. Everything behind them expired Pending, having never
created a pod.

The lost attempt was not the worst of it. A spec-level deadline is
workflow-wide, so once `implement` had consumed it, `implement-fallback`
and `commit-and-push` were both failed instantly with "Step exceeded its
deadline" — and `commit-and-push` is the only step that writes
`.status.yaml` back. The proposal stayed `accepted` with no branch, no PR
and no failure recorded anywhere, which is the exact state the
`assert-produced` truthful-status design exists to prevent.

On the night of 2026-09-19 six of nine implement runs were lost this way:
mctl-agents#333 #334, mctl-telegram#438 #569, mctl-gitops#903 #1178. The
controller log is unambiguous —

    "...isn't at the front" lockType=mutex
        name=argo-workflows/Mutex/mctl-agents-proposal-claims
    "Could not acquire lock named: .../mctl-agents-proposal-claims"
    "Workflow pod is missing" nodePhase=Pending recentlyStarted=false

and the lock timeline matches the outcomes exactly: `bcc54a61` held it
until 01:58, `0eaa9853` (telegram#438) acquired at 01:58 against its own
02:12:31 deadline, got 14 of the 40-70 min it needed, and died.

The 7200s budget moves to the `run-implementer` template, where a
deadline is applied to the pod and therefore runs from the pod's start,
after the mutex is acquired. It covers `implement` and `implement-fallback`
alike — both are that one template — so a primary that burns its full
budget no longer leaves the fallback nothing to run in.

Spec level keeps a ceiling, but as a runaway guard rather than a work
budget: 8h covers worst-case work (7200 + 7200 + a bounded commit) plus
room to queue behind two or three other runs. `commit-and-push` gets its
own 900s so that, without the tight spec ceiling, the step that records
the outcome cannot be the one that hangs.

A deep enough queue can still expire at the spec ceiling — but it now
expires having attempted nothing, rather than discarding a finished
attempt. Narrowing the mutex to the discovery+claim critical section it
actually protects is the real fix for the queue, and is tracked
separately. The precedent is in this same file: `mctl-gitops-main-writes`
was moved off the workflow spec onto `commit-and-push` for exactly this
reason, after holding it across the whole implementer run starved other
main-writers.

Verified with `kubectl apply --dry-run=server`.

Claude-Session: https://claude.ai/code/session_01NFS84XSBZ6vMKsd7CQr6Bf
Copilot AI lite review requested due to automatic review settings September 19, 2026 06:46
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@claude

claude Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

Claude finished @mashkovd's task in 2m 13s —— View job


Review complete ✅

  • Fetch context (PR body, diff 3e911fb...8901a6d)
  • Read full cwft-mctl-agents-implement.yaml for surrounding context (mutex, retryStrategy, validator scripts)
  • Cross-check against scripts/validate-agent-platform.py and the implementer-default ExecutionProfile / policy.yaml ceiling
  • Submit verdict

Verdict: APPROVE — no P1/P2 findings, 0 P3.

Analysis:

  • run-implementer's new activeDeadlineSeconds: 7200 is a template (pod-level) deadline, so it starts once the pod is scheduled — i.e. after the mctl-agents-proposal-claims mutex is acquired. That correctly stops queue time from being charged against the work budget, which is the bug this PR fixes.
  • It covers implement and implement-fallback identically since both use this one template, so a primary that burns its full budget no longer starves the fallback — matches the stated intent.
  • commit-and-push's new activeDeadlineSeconds: 900 is generous for a clone + rebase/push loop and, combined with its existing retryStrategy (limit 3, 30s→5m backoff), tops out around ~45 min worst case — well inside the new 8h ceiling.
  • Worst-case budget math checks out: 7200 (primary) + 7200 (fallback) + ~2700 (commit retries) ≈ 4.75h, leaving real headroom under the 28800s (8h) spec ceiling for queuing behind 2-3 other runs, as described.
  • Verified this doesn't silently break scripts/validate-agent-platform.py's profile↔CWFT timeout cross-check: that check only falls back to spec.activeDeadlineSeconds when a CWFT declares no *_TIMEOUT_SECONDS env var. cwft-mctl-agents-implement.yaml still declares IMPLEMENTER_TIMEOUT_SECONDS=5400 (untouched by this diff), so the effective timeout it compares against implementer-default profile's timeoutSeconds: 5400 (and against policy.yaml's maxTimeoutSeconds: 7200) is unaffected by raising the spec-level ceiling to 28800.
  • Consistent with the precedent cited in the PR body/file comments: mctl-gitops-main-writes was previously moved off the spec-level lock onto commit-and-push for the same "don't hold a wide scope for a long-running step" reason.

No test suite applies to this repo beyond helm lint / terraform validate (not relevant to Argo Workflow YAML), and the PR notes kubectl apply --dry-run=server was already used to validate the manifest.

@github-actions

Copy link
Copy Markdown
Contributor

Antigravity (agy) review — informational

Model: gemini-3.8-flash-medium · commit 8901a6d.

Finding 1: Invalid activeDeadlineSeconds field on run-implementer template

  • Severity: P1
  • File: platform-gitops/argo-workflows/cluster-templates/cwft-mctl-agents-implement.yaml, ~line 371
  • Anchor: + activeDeadlineSeconds: 7200
  • Concrete Failure Scenario: In Argo Workflows, activeDeadlineSeconds is a field on WorkflowSpec (and Kubernetes PodSpec), but does not exist on io.argoproj.workflow.v1alpha1.Template. Adding activeDeadlineSeconds: 7200 directly to the template will cause kubectl apply or ArgoCD GitOps sync to fail schema validation with unknown field "activeDeadlineSeconds". If schema validation is bypassed or relaxed, the field will be discarded during deserialization into the Go struct, leaving the run-implementer template with no timeout. If a model stream or child process hangs, the pod can run up to the 8-hour workflow-level deadline, exhausting API budgets and holding the mctl-agents-proposal-claims mutex for up to 8 hours.
  • Proposed Fix: To apply a pod-level activeDeadlineSeconds that runs from pod start (after the mutex is acquired), inject it via podSpecPatch, or use Argo's native template-level timeout:
    podSpecPatch: '{"activeDeadlineSeconds": 7200}'
    or:
    timeout: "7200s"

Finding 2: Invalid activeDeadlineSeconds field on commit-and-push template

  • Severity: P1
  • File: platform-gitops/argo-workflows/cluster-templates/cwft-mctl-agents-implement.yaml, ~line 768
  • Anchor: + activeDeadlineSeconds: 900
  • Concrete Failure Scenario: Similar to Finding 1, activeDeadlineSeconds is not a valid field on Template. Applying this manifest will fail ArgoCD / Kubernetes CRD validation. If unmarshaled without validation, the field is dropped, leaving commit-and-push without the intended 15-minute deadline. If git operations hang during rebase or push, the step will hold the shared cluster-wide mutex mctl-gitops-main-writes for up to the workflow's 8-hour ceiling, blocking all other workflows that write to platform-gitops main (such as cwft-mctl-agents-run and tenant deletion workflows).
  • Proposed Fix: Use podSpecPatch so the deadline is enforced per pod attempt, or set template-level timeout:
    podSpecPatch: '{"activeDeadlineSeconds": 900}'
    or:
    timeout: "900s"

Finding 3: Missing workflow deadline tests

  • Severity: P3
  • File: platform-gitops/argo-workflows/cluster-templates/cwft-mctl-agents-implement.yaml, ~line 76
  • Anchor: + activeDeadlineSeconds: 28800
  • Concrete Failure Scenario: No test visible in this diff verifying that the raised workflow-level deadline (28800s) and template timeout boundaries behave as expected, nor is there a manifest validation test catching unknown fields on ClusterWorkflowTemplate templates.
  • Proposed Fix: Add a test under tests/ that runs schema validation/linting on all ClusterWorkflowTemplates (e.g., via argo lint or kubeconform against Argo Workflows CRD schemas) to catch invalid fields before merging.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No P1/P2 findings (0 P3). Deadline placement is correct: run-implementer's new 7200s activeDeadlineSeconds is a POD-level deadline that starts after the mctl-agents-proposal-claims mutex is acquired, matching the intent of excluding queue time. The 8h spec ceiling is consistent with worst case (7200 primary + 7200 fallback + up to ~3×900s+backoff commit-and-push retries ≈ 4.75h) plus queue headroom. Verified this doesn't break scripts/validate-agent-platform.py's profile/CWFT timeout cross-check: cwft-mctl-agents-implement.yaml still declares IMPLEMENTER_TIMEOUT_SECONDS=5400, so that check derives its effective timeout from the env var (unchanged) rather than spec.activeDeadlineSeconds, so raising the spec ceiling to 28800 doesn't drift it out of sync with implementer-default profile.yaml's timeoutSeconds: 5400 or policy.yaml's maxTimeoutSeconds: 7200 ceiling. Good to merge.

@mashkovd

Copy link
Copy Markdown
Member Author

Response to the agy review on 8901a6d

Findings 1 and 2 (both P1) are dismissed: the premise is factually wrong.

Both rest on the claim that activeDeadlineSeconds "does not exist on
io.argoproj.workflow.v1alpha1.Template". It does, and it has since long before
the version we run.

  1. Upstream source at the exact version deployed here. The cluster runs
    quay.io/argoproj/workflow-controller:v3.7.11. In
    pkg/apis/workflow/v1alpha1/workflow_types.go at tag v3.7.11, inside
    type Template struct:

    // Optional duration in seconds relative to the StartTime that the pod may be active on a node
    // before the system actively tries to terminate the pod; value must be positive integer
    // This field is only applicable to container and script templates.
    ActiveDeadlineSeconds *intstr.IntOrString `json:"activeDeadlineSeconds,omitempty" protobuf:"bytes,21,opt,name=activeDeadlineSeconds"`

    That doc comment is also exactly the semantics this PR wants: a pod-level
    deadline measured from pod start, i.e. after the mctl-agents-proposal-claims
    mutex has been acquired — which is the entire point of the change.

  2. The applicability caveat is satisfied. "Only applicable to container and
    script templates": run-implementer is a container: template
    (cwft-mctl-agents-implement.yaml:370), commit-and-push is a script:
    template (:784). Neither is a steps/DAG template.

  3. Strict-decoding lint does not flag it. argo lint --offline with a
    newer CLI (v4.1.3) parses this file in strict mode and reports exactly two
    unknown fields — spec.templates[1].synchronization.mutex and
    spec.templates[2].synchronization.mutex, which is the known v3→v4 rename of
    mutex to mutexes — and says nothing about activeDeadlineSeconds on
    either template. If the field were unknown on Template, strict decoding is
    precisely what would have caught it.

  4. Server-side apply accepts it: kubectl apply --dry-run=server returns
    clusterworkflowtemplate.argoproj.io/mctl-agents-implement configured. (On
    its own this is weak — Argo's CRD schema preserves unknown fields — which is
    why points 1–3 are the actual proof.)

The proposed alternatives are not needed. podSpecPatch would reach the same
place by a longer route, and template-level timeout is a controller-side
node timeout with different failure semantics (Failed node with a timeout
message vs. a pod killed by the kubelet), which is not what we want for a step
that holds a mutex.

Finding 3 (P3) — noted, not actioned here. A kubeconform-style CRD-schema
check would not have caught anything in this diff either way, since the Argo
CRDs ship with pruned, unknown-field-preserving schemas; the check with real
teeth is version-matched argo lint, and wiring that into CI is its own change,
not a rider on a deadline hotfix.

Merging on: Claude APPROVED (0 P1/P2/P3), all required checks green, Codex and
Copilot both out of quota on this head.

@mashkovd
mashkovd merged commit 457af1e into main Sep 19, 2026
20 checks passed
@mashkovd
mashkovd deleted the fix/implementer-deadline-counts-queue-time branch September 19, 2026 06:55
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