Skip to content

[3479 by Claude Opus 5.5] Defer jobs whose agent is at its ceiling instead of holding a worker, and charge the actual direct agent - #3497

Open
propr-dev[bot] wants to merge 2 commits into
3478-epic-per-agent-9uvfrom
3479/claude-opus-5-5-defer-jobs-whose-agent-is-20261112-1539-3bs
Open

propr-dev[bot] wants to merge 2 commits into
3478-epic-per-agent-9uvfrom
3479/claude-opus-5-5-defer-jobs-whose-agent-is-20261112-1539-3bs

Conversation

@propr-dev

@propr-dev propr-dev Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

AI Implementation Summary

Closes #3479

Branch: 3479/claude-opus-5-5-defer-jobs-whose-agent-is-20261112-1539-3bs
Commits: ✅ Changes committed (0cfaca1)


AI Processing Completed

Execution Details:

Summary:
The monitor only expired; it doesn't need re-arming, since the batch had already finished. No test failed because of this change, so my earlier summary stands.

Detailed Logs:

  • Conversation: 153 messages
  • Session: ddd90f9b-7015-4a78-9bce-df6651bb8687

Log files stored at:

  • conversation: /usr/src/app/logs/executions/issue-3479-2026-11-12T16-41-13-936Z-conversation.json
  • output: /usr/src/app/logs/executions/issue-3479-2026-11-12T16-41-13-936Z-output.txt
Latest Conversation Messages
ASSISTANT: A job whose agent is at its per-agent ceiling now gives its worker back and waits in the queue. It no longer polls at container create. Everything passes: the related suites (39 files, plus the new te...

ASSISTANT: [content unavailable]

ASSISTANT: The monitor only expired; it doesn't need re-arming, since the batch had already finished. No test failed because of this change, so my earlier summary stands.


This PR was created automatically by ProPR after processing issue #3479.


💡 Need changes?

Comment on this PR to request refinements — the AI agent monitors comments and will update the implementation based on your feedback. Keep iterating until you're satisfied!


Visual preview

Waiting for Agent Capacity timeline row

Waiting for Agent Capacity timeline row

Task details at 1440px for an issue deferred four times because its direct agent local-llama is at its per-agent ceiling: one waiting row, updated in place, with the alias, the deferral count and the next retry time

Connect sign-in required. Original retained until 2027-02-10T16:41:19.293Z.

View the full-resolution original in ProPR Connect

…had already finished. No test failed because of this change, so my earlier summary stands.
@propr-dev

propr-dev Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

/ultrafix goal=8 max=10
Triggered automatically by the requested execution settings.

@propr-dev propr-dev Bot added the ultrafix label Nov 12, 2026
@propr-dev

propr-dev Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

🔄 Ultrafix loop started (goal: 8/10, max cycles: 10)

First action: /review

💡 Tip: Remove the ultrafix label from this PR to stop further ultrafix cycles.

@propr-dev

propr-dev Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor Author

✅ AI Code Review Complete requested by @propr-ultrafix

Posted 1 review:

View Task Details

@propr-dev

propr-dev Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

🔍 AI Code Review — codex:gpt-6-astra

Overall Evaluation

The PR adds early capacity checks, queue deferral, synthetic-member selection, and timeline reporting across the main job types. It needs changes before merge because the new pre-check can prevent recovery of abandoned capacity holders.

✅ Preserved retry budget — Deferrals use moveToDelayed followed by DelayedError, with integration coverage asserting unchanged attemptsMade.

✅ Separated capacity failures — Provider replacement and synthetic failover explicitly exclude capacity refusals.

✅ Useful waiting state — Repeated deferrals update one timeline entry with aliases, count, and retry time.

The supplied current-head status has no failures and five pending checks. This review is static; no commands or tests were run.

Merge blockers

Every finding below was introduced by this PR and must be resolved before merging.

F1: 🔴 Allow abandoned holders to reach recovery

  • Required behavior: Capacity deferral must preserve the existing admission recovery lifecycle; an abandoned execution must not permanently prevent subsequent jobs from running.

  • Evidence: packages/core/src/agents/agentCapacity.ts, agentAliasHasCapacity; src/jobs/agentCapacityDeferral.ts, checkJobAgentCapacity; packages/core/src/agents/agentRunAdmission.ts, prepareAdmittedAgent.

    1. Configure a direct alias with ceiling 1. Its holder process dies without releasing its admission record.
    2. After the holder deadline expires, a subsequent queue job for that alias calls the new pre-check. The holder still counts: expiry does not release capacity, and recovery must confirm container removal.
    3. agentAliasHasCapacity reads the holder count and returns false. The job persists its refusal and moves to delayed before reaching prepareAdmittedAgent.
    4. Every later delivery repeats that same count check. With only jobs for this alias arriving, none reaches the admission loop’s await recover(identity), and the dead process cannot call finish().

    The queue remains indefinitely deferred despite the holder being recoverable. Backoff retries only repeat the pre-check; wake-ups require a release that never occurs. The supplied lifecycle documentation explicitly identifies recovery as demand-driven rather than a separate daemon. The new tests exercise normal holder release, not holder-process death followed exclusively by deferred jobs.

    static trace: Traced the pre-check/refusal path against the admission loop and documented recovery contract. proposed regression: Abandon a ceiling-1 holder, expire its lease, then submit only jobs for that alias and verify recovery and eventual execution.

  • Minimum fix: Make the pre-check recovery-aware: allow jobs encountering expired or recovering holders to reach authoritative recovery, or invoke the existing fenced recovery mechanism before deferring. Preserve confirmed container removal before capacity reuse.

Suggestions

These are optional follow-ups and are not sent to /fix.

No suggestions.

Score

The implementation has substantial coverage and clear integration across job types, but the abandoned-holder recovery regression can indefinitely stall an alias. Correct that path before merge and confirm the pending checks complete successfully.

Score: 6/10


🤖 Review Details

  • Model: GPT-6 Astra
  • Time: 1m 2s
  • Tokens: 91,258 (90,106 in / 1,152 out)
  • Cost: $0.85

View Task


💡 Next step: Comment /fix to address every F# merge blocker, /fix all for every pending blocker and suggestion, or name records explicitly, as in /fix F3 S5.
F# and S# IDs increment across review comments and remain permanent, so selectors such as /fix F3 F5 stay unambiguous across cycles. S# suggestions stay optional: they are implemented only when you name them or request /fix all, and they never relax a merge blocker.

Copy and edit to choose records from this review:

/fix F1

🤖 Review by ProPR

@propr-dev

propr-dev Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor Author

✅ Applied the requested follow-up changes in commit 2cf21c7

Addressed 1 AI review comment (IDs: 6829966296)

Addressed finding F1

Summary of Changes

I fixed F1: a job whose agent's holder died is no longer deferred forever. It now gets through to admission, which recovers the dead holder and then runs the job.

Cause. The capacity pre-check counted every holder record for the alias. A holder whose lease had expired, or one already marked recovering, still counted, so every job for that alias was deferred before it reached the admission loop. That loop is the only place recovery runs, so nothing could ever free the slot.

Fix (in packages/core/src/agents/agentCapacity.ts):

  • New read-only helper. agentAdmissionAliasOccupancy splits each alias's holders into live and recoverable (lease expired, or recovering). It uses Redis server time, the same clock the admission script uses to expire leases.
  • Pre-check counts only live holders. If a job only meets recoverable holders, it goes on to the normal acquire. That calls recover, which removes the recorded container before releasing the holder, so a slot is still never reused until removal is confirmed. If removal fails, the slot stays held, as before.
  • The two other callers get the same rule. Synthetic routing goes through the same pre-check. The wake-up on release (wakeAgentCapacityWaiters) now counts only live holders when working out free slots, so a woken job can be the one that does the recovery.
  • No change to the admission script. agentRunAdmission.ts is untouched; I moved the helper out of it because it pushed the file over the 400-line lint limit.
  • Docs. docs/docs/architecture/agent-run-admission.md now explains the live-holder rule for the pre-check and the wake-up.

New test in test/agentCapacityDeferral.test.ts, run against a real Redis 7.4.1 and BullMQ:

  1. A holder on a ceiling-1 alias "dies": its lease renewal never completes and it never calls finish().
  2. A job for that alias is deferred while the holder is still live, and the wake-up finds no free slot.
  3. After the lease expires, the holder record is still there, but the pre-check reports room and the wake-up promotes the waiting job.
  4. The job runs. The dead container is removed before the job's own container is created, it used one attempt, and its deferral data is cleared.
  5. A later job for the same alias runs without being deferred, and no holder records are left.

With the old pre-check put back, this test fails while the other five in the file pass.

Verification:

  • agentCapacityDeferral passed 6/6 three times in a row.
  • Related suites pass: agentRunAliasAdmission (10), agentRunAdmission (27), reviewCapacityRouting (2), and core's syntheticRoutingCapacity and syntheticRoutingService (19).
  • Typecheck (root and core) and eslint --max-warnings 0 on the core agents directory are clean.

One unrelated flaky test: in one of three agentRunAdmission runs, "caller deadline during create removes the ID without starting it" timed out at 15 s. It forks worker processes, doesn't depend on any file I changed, and passed 4 more times in a row both with and without this fix.

There's no visual preview because nothing in the UI changed.


🤖 Implementation Details

  • Model: Claude Opus 5.5 (medium)
  • Requested By: @propr-ultrafix
  • Time: 15m 18s
  • Tokens: 2,372,884 (2,354,478 in / 18,406 out)
  • Cost: $1.26
  • Subscription usage: Fable +1%

View Task Execution


💡 ProPR Slash Commands
Command Description Example
/merge Merge target branch into this PR and resolve conflicts /merge
/review Request an AI code review /review or /review claude-sonnet
/fix Address review blockers, or blockers and suggestions with /fix all /fix, /fix all, or /fix F3 S5
/switch Change the AI model for this PR /switch claude-opus
/use Override the model for a single follow-up run /use claude-sonnet
/ultrafix Loop review→fix cycles until score goal is met /ultrafix or /ultrafix goal=8 max=10

🤖 Generated by ProPR

…ferred forever. It now gets through to admission, which recovers the dead holder and then runs the job.

I fixed F1: a job whose agent's holder died is no longer deferred forever. It now gets through to admission, which recovers the dead holder and then runs the job.

**Cause.** The capacity pre-check counted every holder record for the alias. A holder whose lease had expired, or one already marked `recovering`, still counted, so every job for that alias was deferred before it reached the admission loop. That loop is the only place recovery runs, so nothing could ever free the slot.

**Fix** (in `packages/core/src/agents/agentCapacity.ts`):
- **New read-only helper.** `agentAdmissionAliasOccupancy` splits each alias's holders into `live` and `recoverable` (lease expired, or `recovering`). It uses Redis server time, the same clock the admission script uses to expire leases.
- **Pre-check counts only live holders.** If a job only meets recoverable holders, it goes on to the normal acquire. That calls `recover`, which removes the recorded container before releasing the holder, so a slot is still never reused until removal is confirmed. If removal fails, the slot stays held, as before.
- **The two other callers get the same rule.** Synthetic routing goes through the same pre-check. The wake-up on release (`wakeAgentCapacityWaiters`) now counts only live holders when working out free slots, so a woken job can be the one that does the recovery.
- **No change to the admission script.** `agentRunAdmission.ts` is untouched; I moved the helper out of it because it pushed the file over the 400-line lint limit.
- **Docs.** `docs/docs/architecture/agent-run-admission.md` now explains the live-holder rule for the pre-check and the wake-up.

**New test** in `test/agentCapacityDeferral.test.ts`, run against a real Redis 7.4.1 and BullMQ:
1. A holder on a ceiling-1 alias "dies": its lease renewal never completes and it never calls `finish()`.
2. A job for that alias is deferred while the holder is still live, and the wake-up finds no free slot.
3. After the lease expires, the holder record is still there, but the pre-check reports room and the wake-up promotes the waiting job.
4. The job runs. The dead container is removed before the job's own container is created, it used one attempt, and its deferral data is cleared.
5. A later job for the same alias runs without being deferred, and no holder records are left.

With the old pre-check put back, this test fails while the other five in the file pass.

**Verification:**
- `agentCapacityDeferral` passed 6/6 three times in a row.
- Related suites pass: `agentRunAliasAdmission` (10), `agentRunAdmission` (27), `reviewCapacityRouting` (2), and core's `syntheticRoutingCapacity` and `syntheticRoutingService` (19).
- Typecheck (root and core) and `eslint --max-warnings 0` on the core agents directory are clean.

One unrelated flaky test: in one of three `agentRunAdmission` runs, "caller deadline during create removes the ID without starting it" timed out at 15 s. It forks worker processes, doesn't depend on any file I changed, and passed 4 more times in a row both with and without this fix.

There's no visual preview because nothing in the UI changed.

PR: #3497
Comment by: @propr-ultrafix (ID: 0)
Model: Claude Opus 5.5
@propr-dev

propr-dev Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

⏳ Ultrafix is waiting for CI before the next /review.

These blocking checks on 2cf21c7 are holding the loop back:

  • Restricted network container smoke — not finished
  • Classify changed surfaces — not finished
  • Validate Changes — not finished
  • Full Test Suite Shard 4/4 — not finished
  • Full Test Suite Shard 1/4 — not finished
  • Full Test Suite Shard 2/4 — not finished
  • Full Test Suite Shard 3/4 — not finished
  • Classify changed surfaces — not finished
  • Classify changed surfaces — not finished
  • Classify changed surfaces — not finished
  • Classify changed surfaces — not finished

Checks matching this repository's non-blocking checks never gate Ultrafix. The loop continues automatically once the blocking checks pass; if they have not settled within 2 hours, Ultrafix stops.

@propr-dev

propr-dev Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

CI failed: Full Test Suite Shard 1/4

Please investigate and fix this CI failure.

  • Check: Full Test Suite Shard 1/4
  • Result: failure
  • Commit: 2cf21c7f26e6 (2cf21c7f26e62b7144a5187b891ee0ac5824699d)
  • Details: View CI failure

Failure evidence

.github:11
Process completed with exit code 1.

.github:42789
Process completed with exit code 1.

.github:2
Node.js 20 is deprecated. The following actions target Node.js 20 but are being forced to run on Node.js 24: actions/checkout@11d5960. For more information see: https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/

@propr-dev

propr-dev Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

🔄 Starting work on follow-up changes requested by @propr-dev[bot]

I'll analyze the 1 request and implement the necessary changes.

View Task Progress


Processing comment ID: 6830744846✓

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants