Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions actions/setup/js/claude_harness.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const {
} = require("./awf_reflect.cjs");
const { emitMissingToolPermissionIssue, hasNoopInSafeOutputs } = require("./safeoutputs_cli.cjs");
const { countPermissionDeniedIssues, hasNumerousPermissionDeniedIssues, extractDeniedCommands, buildMissingToolPermissionIssuePayload } = require("./permission_denied_helpers.cjs");
const { detectNonRetryableHarnessGuard } = require("./harness_retry_guard.cjs");

// Maximum number of retry attempts after the initial run
const MAX_RETRIES = 3;
Expand Down Expand Up @@ -396,6 +397,15 @@ async function main() {
break;
}

const nonRetryableGuard = detectNonRetryableHarnessGuard(result.output);
if (nonRetryableGuard.aiCreditsExceeded || nonRetryableGuard.awfAPIProxyBlockingRequests) {
const reasons = [];
if (nonRetryableGuard.aiCreditsExceeded) reasons.push("AI credits budget exceeded");
if (nonRetryableGuard.awfAPIProxyBlockingRequests) reasons.push("AWF API proxy is blocking requests");
log(`attempt ${attempt + 1}: ${reasons.join(" and ")} — not retrying (non-retryable guard condition)`);
break;
}

if (attempt === 0 && isAuthenticationFailed) {
log(`attempt ${attempt + 1}: authentication failed — not retrying (first-attempt auth failure is non-retryable)`);
break;
Expand Down
10 changes: 10 additions & 0 deletions actions/setup/js/codex_harness.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const {
} = require("./awf_reflect.cjs");
const { emitMissingToolPermissionIssue, hasNoopInSafeOutputs } = require("./safeoutputs_cli.cjs");
const { countPermissionDeniedIssues, hasNumerousPermissionDeniedIssues, extractDeniedCommands, buildMissingToolPermissionIssuePayload } = require("./permission_denied_helpers.cjs");
const { detectNonRetryableHarnessGuard } = require("./harness_retry_guard.cjs");

// Maximum number of retry attempts after the initial run
const MAX_RETRIES = 3;
Expand Down Expand Up @@ -425,6 +426,15 @@ async function main() {
break;
}

const nonRetryableGuard = detectNonRetryableHarnessGuard(result.output);
if (nonRetryableGuard.aiCreditsExceeded || nonRetryableGuard.awfAPIProxyBlockingRequests) {
const reasons = [];
if (nonRetryableGuard.aiCreditsExceeded) reasons.push("AI credits budget exceeded");
if (nonRetryableGuard.awfAPIProxyBlockingRequests) reasons.push("AWF API proxy is blocking requests");
log(`attempt ${attempt + 1}: ${reasons.join(" and ")} — not retrying (non-retryable guard condition)`);
break;
}

if (attempt === 0 && isAuthenticationFailed) {
log(`attempt ${attempt + 1}: authentication failed — not retrying (first-attempt auth failure is non-retryable)`);
break;
Expand Down
10 changes: 10 additions & 0 deletions actions/setup/js/copilot_harness.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const {
} = require("./awf_reflect.cjs");
const { runSafeOutputsCLI, buildMissingToolAlternatives, emitMissingToolPermissionIssue, emitInfrastructureIncomplete, hasNoopInSafeOutputs } = require("./safeoutputs_cli.cjs");
const { countPermissionDeniedIssues, hasNumerousPermissionDeniedIssues, extractDeniedCommands, buildMissingToolPermissionIssuePayload } = require("./permission_denied_helpers.cjs");
const { detectNonRetryableHarnessGuard } = require("./harness_retry_guard.cjs");

// Maximum number of retry attempts after the initial run
const MAX_RETRIES = 3;
Expand Down Expand Up @@ -737,6 +738,15 @@ async function main() {
break;
}

const nonRetryableGuard = detectNonRetryableHarnessGuard(result.output);
if (nonRetryableGuard.aiCreditsExceeded || nonRetryableGuard.awfAPIProxyBlockingRequests) {
const reasons = [];
if (nonRetryableGuard.aiCreditsExceeded) reasons.push("AI credits budget exceeded");
if (nonRetryableGuard.awfAPIProxyBlockingRequests) reasons.push("AWF API proxy is blocking requests");
log(`attempt ${attempt + 1}: ${reasons.join(" and ")} — not retrying (non-retryable guard condition)`);
break;
}

if (attempt === 0 && isAuthenticationFailed) {
if (proxyAuthDiagnostic) {
log(`attempt ${attempt + 1}: ${proxyAuthDiagnostic} — not retrying (first-attempt auth failure is non-retryable)`);
Expand Down
28 changes: 28 additions & 0 deletions actions/setup/js/harness_retry_guard.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// @ts-check

"use strict";

const AI_CREDITS_EXCEEDED_PATTERNS = [/\bmax[\s_-]*ai[\s_-]*credits[\s_-]*exceeded\b/i, /\bai[\s_-]*credits[\s_-]*rate[\s_-]*limit[\s_-]*error\b/i, /ai[\s_-]*credits?.*(?:rate[\s-]*limit|limit exceeded|budget exceeded|exceeded)/i];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/diagnose] All three AI-credits patterns are on one long line, making them very hard to review for correctness and nearly impossible to diff in future PRs.

💡 One pattern per line
const AI_CREDITS_EXCEEDED_PATTERNS = [
  /\bmax[\s_-]*ai[\s_-]*credits[\s_-]*exceeded\b/i,
  /\bai[\s_-]*credits[\s_-]*rate[\s_-]*limit[\s_-]*error\b/i,
  /ai[\s_-]*credits?.*(?:rate[\s-]*limit|limit exceeded|budget exceeded|exceeded)/i,
];

Note also that the third pattern lacks a leading \b word-boundary unlike the first two — worth confirming that is intentional (it broadens the match surface).


const AWF_API_PROXY_BLOCKING_REQUESTS_PATTERNS = [/\bawf\b.*\bapi[\s_-]*proxy\b.*\bblocking requests\b/i, /\bapi[\s_-]*proxy\b.*\bblocking requests\b/i, /\bapi[\s_-]*proxy\b.*\bblocked requests?\b/i, /\bDIFC_FILTERED\b/];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/diagnose] Same readability issue: all four AWF proxy patterns are on one line. Splitting to one-per-line makes each pattern independently reviewable and auditable.

💡 Suggested formatting
const AWF_API_PROXY_BLOCKING_REQUESTS_PATTERNS = [
  /\bawf\b.*\bapi[\s_-]*proxy\b.*\bblocking requests\b/i,
  /\bapi[\s_-]*proxy\b.*\bblocking requests\b/i,
  /\bapi[\s_-]*proxy\b.*\bblocked requests?\b/i,
  /\bDIFC_FILTERED\b/,
];


/**
* Detect retry guard conditions that should stop harness retries immediately.
* @param {unknown} output
* @returns {{ aiCreditsExceeded: boolean, awfAPIProxyBlockingRequests: boolean }}
*/
function detectNonRetryableHarnessGuard(output) {
const safeOutput = typeof output === "string" ? output : "";
Comment on lines +9 to +15
return {
aiCreditsExceeded: AI_CREDITS_EXCEEDED_PATTERNS.some(pattern => pattern.test(safeOutput)),
awfAPIProxyBlockingRequests: AWF_API_PROXY_BLOCKING_REQUESTS_PATTERNS.some(pattern => pattern.test(safeOutput)),
};
}

if (typeof module !== "undefined" && module.exports) {
module.exports = {
detectNonRetryableHarnessGuard,
AI_CREDITS_EXCEEDED_PATTERNS,
AWF_API_PROXY_BLOCKING_REQUESTS_PATTERNS,
};
}
69 changes: 69 additions & 0 deletions actions/setup/js/harness_retry_guard.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// @ts-check

import { describe, expect, it } from "vitest";
import { createRequire } from "node:module";

const require = createRequire(import.meta.url);
const { detectNonRetryableHarnessGuard } = require("./harness_retry_guard.cjs");

describe("harness_retry_guard.cjs", () => {
it("detects AI credits exceeded markers", () => {
const result = detectNonRetryableHarnessGuard("error: max_ai_credits_exceeded=true");
expect(result.aiCreditsExceeded).toBe(true);
expect(result.awfAPIProxyBlockingRequests).toBe(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] Only the first AI-credits pattern (max_ai_credits_exceeded) is exercised. The second and third patterns are untested, so a regression in those regexes would go undetected.

💡 Add tests for the other two patterns
it("detects ai_credits_rate_limit_error signal", () => {
  const result = detectNonRetryableHarnessGuard("error: ai_credits_rate_limit_error=true");
  expect(result.aiCreditsExceeded).toBe(true);
});

it("detects broad ai-credits-budget-exceeded signal", () => {
  const result = detectNonRetryableHarnessGuard("ai credits budget exceeded");
  expect(result.aiCreditsExceeded).toBe(true);
});

Each regex in AI_CREDITS_EXCEEDED_PATTERNS represents a distinct signal variant. One test per pattern ensures the full detection surface is validated.

});

it("detects AI credits rate-limit markers", () => {
const result = detectNonRetryableHarnessGuard("error: ai_credits_rate_limit_error=true");
expect(result.aiCreditsExceeded).toBe(true);
expect(result.awfAPIProxyBlockingRequests).toBe(false);
});

it("detects AI credits budget markers", () => {
const result = detectNonRetryableHarnessGuard("error: ai credits budget exceeded");
expect(result.aiCreditsExceeded).toBe(true);
expect(result.awfAPIProxyBlockingRequests).toBe(false);
});

it("detects AWF API proxy blocking request markers", () => {
const result = detectNonRetryableHarnessGuard("awf api proxy is blocking requests for this run");
expect(result.aiCreditsExceeded).toBe(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] Only the first AWF proxy pattern (full phrase with "awf") is tested in the positive direction; the second (api-proxy.*blocking requests) and third (api-proxy.*blocked requests) patterns have no dedicated test.

💡 Cover the remaining AWF proxy patterns
it("detects api-proxy blocking requests without awf prefix", () => {
  const result = detectNonRetryableHarnessGuard("api-proxy is blocking requests");
  expect(result.awfAPIProxyBlockingRequests).toBe(true);
});

it("detects api-proxy blocked requests variant", () => {
  const result = detectNonRetryableHarnessGuard("api proxy blocked request");
  expect(result.awfAPIProxyBlockingRequests).toBe(true);
});

Driving one test per pattern (rather than one test per signal type) keeps each regex independently verifiable and makes failures self-diagnosing.

expect(result.awfAPIProxyBlockingRequests).toBe(true);
});

it("detects API proxy blocking request markers without AWF prefix", () => {
const result = detectNonRetryableHarnessGuard("api-proxy is blocking requests");
expect(result.aiCreditsExceeded).toBe(false);
expect(result.awfAPIProxyBlockingRequests).toBe(true);
});

it("detects API proxy blocked request markers", () => {
const result = detectNonRetryableHarnessGuard("api proxy blocked request");
expect(result.aiCreditsExceeded).toBe(false);
expect(result.awfAPIProxyBlockingRequests).toBe(true);
});

it("detects DIFC filtered proxy block markers", () => {
const result = detectNonRetryableHarnessGuard('{"type":"DIFC_FILTERED","reason":"blocked"}');
expect(result.aiCreditsExceeded).toBe(false);
expect(result.awfAPIProxyBlockingRequests).toBe(true);
});

it("returns false for non-string input", () => {
const result = detectNonRetryableHarnessGuard(null);
expect(result.aiCreditsExceeded).toBe(false);
expect(result.awfAPIProxyBlockingRequests).toBe(false);
});

it("detects both flags when output contains both signals", () => {
const result = detectNonRetryableHarnessGuard("max_ai_credits_exceeded=true DIFC_FILTERED");
expect(result.aiCreditsExceeded).toBe(true);
expect(result.awfAPIProxyBlockingRequests).toBe(true);
});

it("returns false when output has no guard markers", () => {
const result = detectNonRetryableHarnessGuard("transient network timeout");
expect(result.aiCreditsExceeded).toBe(false);
expect(result.awfAPIProxyBlockingRequests).toBe(false);
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] No test covers non-string input (e.g. null, undefined, an object) or combined signals where both flags are true. These are the boundary conditions most likely to surface a real regression.

💡 Add boundary tests
it("returns false for null input", () => {
  const result = detectNonRetryableHarnessGuard(null);
  expect(result.aiCreditsExceeded).toBe(false);
  expect(result.awfAPIProxyBlockingRequests).toBe(false);
});

it("returns both flags true when output contains both signals", () => {
  const combined = "max_ai_credits_exceeded=true; DIFC_FILTERED";
  const result = detectNonRetryableHarnessGuard(combined);
  expect(result.aiCreditsExceeded).toBe(true);
  expect(result.awfAPIProxyBlockingRequests).toBe(true);
});

The combined-signals test also validates that the log message in each harness correctly emits both reasons separated by " and ".

Loading