Skip to content

Commit 9fba95c

Browse files
chore: sync actions from gh-aw@v0.81.4 (#172)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent a95cb38 commit 9fba95c

18 files changed

Lines changed: 696 additions & 256 deletions

setup/js/assign_agent_helpers.cjs

Lines changed: 77 additions & 171 deletions
Large diffs are not rendered by default.

setup/js/assign_to_agent.cjs

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @ts-check
22
/// <reference types="@actions/github-script" />
33

4-
const { AGENT_LOGIN_NAMES, getAvailableAgentLogins, findAgent, getIssueDetails, getPullRequestDetails, assignAgentToIssue, generatePermissionErrorSummary } = require("./assign_agent_helpers.cjs");
4+
const { AGENT_LOGIN_NAMES, getAgentLogins, getAvailableAgentLogins, findAgent, getIssueDetails, getPullRequestDetails, assignAgentToIssue, generatePermissionErrorSummary } = require("./assign_agent_helpers.cjs");
55
const { getErrorMessage } = require("./error_helpers.cjs");
66
const { resolveTarget, isStagedMode } = require("./safe_output_helpers.cjs");
77
const { generateStagedPreview } = require("./staged_preview.cjs");
@@ -327,15 +327,15 @@ async function main(config = {}) {
327327

328328
try {
329329
// Find agent (use cache to avoid repeated lookups)
330-
let agentId = agentCache[agentName];
331-
if (!agentId) {
330+
let agentLogin = agentCache[agentName];
331+
if (!agentLogin) {
332332
core.info(`Looking for ${agentName} coding agent...`);
333-
agentId = await findAgent(effectiveOwner, effectiveRepo, agentName, issueNumber || pullNumber, githubClient);
334-
if (!agentId) {
333+
agentLogin = await findAgent(effectiveOwner, effectiveRepo, agentName, issueNumber || pullNumber, githubClient);
334+
if (!agentLogin) {
335335
throw new Error(`${agentName} coding agent is not available for this repository`);
336336
}
337-
agentCache[agentName] = agentId;
338-
core.info(`Found ${agentName} coding agent (ID: ${agentId})`);
337+
agentCache[agentName] = agentLogin;
338+
core.info(`Found ${agentName} coding agent (login: ${agentLogin})`);
339339
}
340340

341341
// Get issue or PR details
@@ -371,7 +371,8 @@ async function main(config = {}) {
371371
// Skip if agent is already assigned and no explicit per-item pull_request_repo is specified.
372372
// When a different pull_request_repo is provided on the message, allow re-assignment
373373
// so Copilot can be triggered for a different target repository on the same issue.
374-
if (currentAssignees.some(a => a.id === agentId) && !shouldAllowReassignment) {
374+
const knownLogins = getAgentLogins(agentName);
375+
if (currentAssignees.some(a => a.login === agentLogin || knownLogins.includes(a.login)) && !shouldAllowReassignment) {
375376
core.info(`${agentName} is already assigned to ${type} #${number}`);
376377
_allResults.push({ issue_number: issueNumber, pull_number: pullNumber, agent: agentName, owner: effectiveOwner, repo: effectiveRepo, pull_request_repo: effectivePullRequestRepoSlug, success: true });
377378
return { success: true };
@@ -383,7 +384,7 @@ async function main(config = {}) {
383384
if (customInstructions) core.info(`Using custom instructions: ${customInstructions.substring(0, 100)}${customInstructions.length > 100 ? "..." : ""}`);
384385
if (effectiveBaseBranch) core.info(`Using base branch: ${effectiveBaseBranch}`);
385386

386-
const success = await assignAgentToIssue(assignableId, agentId, currentAssignees, agentName, allowedAgents, model, customAgent, customInstructions, effectiveBaseBranch, githubClient, taskContext, effectivePullRequestRepoSlug);
387+
const success = await assignAgentToIssue(assignableId, agentLogin, currentAssignees, agentName, allowedAgents, model, customAgent, customInstructions, effectiveBaseBranch, githubClient, taskContext, effectivePullRequestRepoSlug);
387388
if (!success) throw new Error(`Failed to assign ${agentName} via REST`);
388389

389390
core.info(`Successfully assigned ${agentName} coding agent to ${type} #${number}`);
@@ -392,14 +393,42 @@ async function main(config = {}) {
392393
} catch (error) {
393394
let errorMessage = getErrorMessage(error);
394395

396+
// When the agent specified an issue_number that turns out to be a PR, skip
397+
// silently without posting a comment — error comments on PRs are confusing.
398+
if (/** @type {any} */ error.isPullRequest) {
399+
core.warning(`Skipping assign_to_agent for #${number}: target is a pull request, not an issue.`);
400+
_allResults.push({
401+
issue_number: issueNumber,
402+
pull_number: pullNumber,
403+
agent: agentName,
404+
owner: effectiveOwner,
405+
repo: effectiveRepo,
406+
pull_request_repo: effectivePullRequestRepoSlug,
407+
success: false,
408+
skipped: true,
409+
error: errorMessage,
410+
});
411+
return { success: false, skipped: true, error: errorMessage };
412+
}
413+
395414
const isAuthError = ["Bad credentials", "Not Authenticated", "Resource not accessible", "Insufficient permissions", "requires authentication"].some(msg => errorMessage.includes(msg));
396415
const isAvailabilityError = errorMessage.includes("coding agent is not available for this repository");
397416

398417
if (ignoreIfError && (isAuthError || isAvailabilityError)) {
399418
const errorType = isAuthError ? "authentication/permission" : "agent availability";
400419
core.warning(`Agent assignment failed for ${agentName} on ${type} #${number} due to ${errorType} error. Skipping due to ignore-if-error=true.`);
401420
core.info(`Error details: ${errorMessage}`);
402-
_allResults.push({ issue_number: issueNumber, pull_number: pullNumber, agent: agentName, owner: effectiveOwner, repo: effectiveRepo, pull_request_repo: effectivePullRequestRepoSlug, success: true, skipped: true });
421+
_allResults.push({
422+
issue_number: issueNumber,
423+
pull_number: pullNumber,
424+
agent: agentName,
425+
owner: effectiveOwner,
426+
repo: effectiveRepo,
427+
pull_request_repo: effectivePullRequestRepoSlug,
428+
success: true,
429+
skipped: true,
430+
error: errorMessage,
431+
});
403432
return { success: true, skipped: true };
404433
}
405434

@@ -451,18 +480,24 @@ function getAssignToAgentAssigned() {
451480

452481
/**
453482
* Returns the "assignment_errors" output string for step outputs.
454-
* Format: "issue:N:agent:error" or "pr:N:agent:error" per failure, newline-separated.
483+
* Format: "issue:N:agent:error" or "pr:N:agent:error" per failure/skipped-with-error,
484+
* newline-separated.
455485
* @returns {string}
456486
*/
457487
function getAssignToAgentErrors() {
458-
return _allResults
459-
.filter(r => !r.success && !r.skipped)
460-
.map(r => {
461-
const number = r.issue_number || r.pull_number;
462-
const prefix = r.issue_number ? "issue" : "pr";
463-
return `${prefix}:${number}:${r.agent}:${r.error}`;
464-
})
465-
.join("\n");
488+
return (
489+
_allResults
490+
// Include skipped(ignore-if-error) entries that still captured an error so
491+
// downstream failure handling can surface assignment problems in issue/comment reports.
492+
// Include hard failures (!success) and ignored failures (skipped=true with error).
493+
.filter(r => r.error && (r.skipped || !r.success))
494+
.map(r => {
495+
const number = r.issue_number || r.pull_number;
496+
const prefix = r.issue_number ? "issue" : "pr";
497+
return `${prefix}:${number}:${r.agent}:${r.error}`;
498+
})
499+
.join("\n")
500+
);
466501
}
467502

468503
/**

setup/js/claude_harness.cjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,10 +410,11 @@ async function main() {
410410
}
411411

412412
const nonRetryableGuard = detectNonRetryableHarnessGuard(result.output);
413-
if (nonRetryableGuard.aiCreditsExceeded || nonRetryableGuard.awfAPIProxyBlockingRequests) {
413+
if (nonRetryableGuard.aiCreditsExceeded || nonRetryableGuard.awfAPIProxyBlockingRequests || nonRetryableGuard.maxRunsExceeded) {
414414
const reasons = [];
415415
if (nonRetryableGuard.aiCreditsExceeded) reasons.push("AI credits budget exceeded");
416416
if (nonRetryableGuard.awfAPIProxyBlockingRequests) reasons.push("AWF API proxy is blocking requests");
417+
if (nonRetryableGuard.maxRunsExceeded) reasons.push("maximum LLM invocations exceeded");
417418
log(`attempt ${attempt + 1}: ${reasons.join(" and ")} — not retrying (non-retryable guard condition)`);
418419
break;
419420
}

setup/js/codex_harness.cjs

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,23 @@ const BACKOFF_MULTIPLIER = 2;
5858
// Maximum delay cap in milliseconds
5959
const MAX_DELAY_MS = 60000;
6060

61-
// Pattern to detect OpenAI rate-limit errors (HTTP 429).
62-
// Matches "rate_limit_exceeded" from the OpenAI error type field and the "429" status code
63-
// that Codex emits when the API rate limit is hit.
64-
const RATE_LIMIT_ERROR_PATTERN = /rate_limit_exceeded|429 Too Many Requests|RateLimitError/i;
61+
// Pattern to detect OpenAI rate-limit errors.
62+
// Matches the JSON error type field ("rate_limit_exceeded"), the HTTP status code
63+
// ("429 Too Many Requests"), the client-side exception class ("RateLimitError"), and
64+
// the human-readable message Codex emits inside "Reconnecting..." / error lines:
65+
// "Rate limit reached for <model> in organization <org> on tokens per min (TPM): ..."
66+
const RATE_LIMIT_ERROR_PATTERN = /rate_limit_exceeded|429 Too Many Requests|RateLimitError|Rate limit reached for [^\s]+(?: in organization [^\s]+)? on tokens per min/i;
67+
68+
// Pattern to detect when Codex's internal stream-reconnect budget is fully spent.
69+
// Codex emits "Reconnecting... N/N (reason)" where both numbers are the same when
70+
// the reconnect is the last allowed attempt. Seeing this pattern together with a
71+
// rate-limit error means the session cannot make forward progress: every reconnect
72+
// attempt immediately fails with the same rate-limit, and a fresh harness run will
73+
// re-encounter the same limit since the same work pattern consumes the same TPM budget.
74+
//
75+
// The backreference \1 requires the two numeric parts of "N/N" to be identical —
76+
// "5/5" matches (exhausted) but "1/5", "3/5", "4/5" do not (still retrying).
77+
const RECONNECT_EXHAUSTED_PATTERN = /Reconnecting\.\.\.\s+(\d+)\/\1\b/;
6578
const AUTHENTICATION_FAILED_PATTERN = /Authentication failed(?:\s*\(Request ID:[^)]+\))?/i;
6679

6780
// Pattern to detect a missing API key at startup — Codex emits this before making any API
@@ -130,6 +143,20 @@ function isInvalidModelError(output) {
130143
return INVALID_MODEL_ERROR_PATTERN.test(output);
131144
}
132145

146+
/**
147+
* Determines if the collected output shows that Codex's internal stream-reconnect
148+
* retries are exhausted (i.e., the output contains "Reconnecting... N/N" where both
149+
* numbers are the same, indicating the last reconnect attempt).
150+
*
151+
* When this is true together with a rate-limit error, retrying from scratch would
152+
* immediately encounter the same rate limit and drain the token budget further.
153+
* @param {string} output - Collected stdout+stderr from the process
154+
* @returns {boolean}
155+
*/
156+
function isReconnectExhaustedError(output) {
157+
return RECONNECT_EXHAUSTED_PATTERN.test(output);
158+
}
159+
133160
/**
134161
* Resolve --prompt-file arguments for the Codex run.
135162
* Strips the --prompt-file <path> pair from args and appends the file content
@@ -439,11 +466,12 @@ async function main() {
439466
}
440467

441468
const nonRetryableGuard = detectNonRetryableHarnessGuard(result.output);
442-
if (nonRetryableGuard.aiCreditsExceeded || nonRetryableGuard.awfAPIProxyBlockingRequests || nonRetryableGuard.goalAlreadyActive) {
469+
if (nonRetryableGuard.aiCreditsExceeded || nonRetryableGuard.awfAPIProxyBlockingRequests || nonRetryableGuard.goalAlreadyActive || nonRetryableGuard.maxRunsExceeded) {
443470
const reasons = [];
444471
if (nonRetryableGuard.aiCreditsExceeded) reasons.push("AI credits budget exceeded");
445472
if (nonRetryableGuard.awfAPIProxyBlockingRequests) reasons.push("AWF API proxy is blocking requests");
446473
if (nonRetryableGuard.goalAlreadyActive) reasons.push("goal is already active for this thread (use update_goal when the current goal is complete)");
474+
if (nonRetryableGuard.maxRunsExceeded) reasons.push("maximum LLM invocations exceeded");
447475
log(`attempt ${attempt + 1}: ${reasons.join(" and ")} — not retrying (non-retryable guard condition)`);
448476
break;
449477
}
@@ -470,6 +498,15 @@ async function main() {
470498
break;
471499
}
472500

501+
// Codex's internal stream-reconnect retries are exhausted and the root cause is a
502+
// rate-limit error. Each reconnect attempt immediately failed with the same limit,
503+
// so a fresh harness run will encounter the same rate-limit at the same point in the
504+
// session and drain the token budget further without making progress.
505+
if (isRateLimit && isReconnectExhaustedError(result.output)) {
506+
log(`attempt ${attempt + 1}: rate-limit with exhausted reconnects — not retrying (fresh run would hit the same rate limit)`);
507+
break;
508+
}
509+
473510
// Retry when the session was partially executed (has output) or on well-known
474511
// transient errors (rate limit, server error) even without output.
475512
const isTransient = isRateLimit || isServer;
@@ -504,6 +541,7 @@ if (typeof module !== "undefined" && module.exports) {
504541
isMissingApiKeyError,
505542
isServerError,
506543
isInvalidModelError,
544+
isReconnectExhaustedError,
507545
countPermissionDeniedIssues,
508546
hasNumerousPermissionDeniedIssues,
509547
extractDeniedCommands,

0 commit comments

Comments
 (0)