Skip to content

Retry badge moderation when reasoning eats the whole budget - #70

Open
boomzero wants to merge 1 commit into
masterfrom
fix/badge-moderation-truncation
Open

Retry badge moderation when reasoning eats the whole budget#70
boomzero wants to merge 1 commit into
masterfrom
fix/badge-moderation-truncation

Conversation

@boomzero

@boomzero boomzero commented Jul 27, 2026

Copy link
Copy Markdown
Member

The bug

glm-4.7-flash reasons before answering, and its reasoning tokens come out of the same max_completion_tokens budget as the answer. A badge the model finds hard could spend the entire 1024-token allowance deliberating and come back with finish_reason: "length" and content: null. ReadModerationVerdict correctly saw nothing usable, EditBadge failed closed, and the user was told 内容审核服务暂时不可用 — when the model had in fact answered at length, just never in JSON.

Found via npm run check-badge -- 陈开尔万岁, which reported UNUSABLE. The model reads 陈开尔 as a disguised profanity homophone and then argues with itself about whether 万岁 makes it rule 1 or rule 11. Six runs at temperature 0:

finish=stop    tokens=521   {"allowed": false, "rule": 1}
finish=stop    tokens=971   {"allowed": true,  "rule": 0}
finish=length  tokens=1024  content=null
finish=length  tokens=1024  content=null
finish=length  tokens=1024  content=null

Half of them never produced a verdict. Any badge that makes the model deliberate this long was unsettable.

The fix

  • BadgeModerationMaxTokens = 2048, replacing the inline 1024.
  • ModerationReplyTruncated() distinguishes "ran out of budget mid-thought" from "answered badly".
  • EditBadge retries once, only on truncation. A complete-but-malformed reply and a thrown error both still fail closed on the first attempt — retrying those would spend a second inference call to hear the same thing.
  • Both constants are exported, so tools/check-badge.js shares them and its neuron figure stays honest about what a real edit costs, retries included. The tool now annotates (retried 1x after truncation).

Cost

Unchanged for ordinary badges — 5 mixed checks came to 123.9 neurons, ~25 each, same as before. The extra headroom is only billed on inputs that actually reason that long.

Verification

Full suite: 95 pass, 0 fail. Six new tests cover retry-lands-a-reject, retry-lands-an-allow, two-truncations-fails-closed-at-exactly-2-calls, malformed-is-not-retried, throw-is-not-retried, and the raised budget assertion.

Against the live model, three runs of the input that started this:

ALLOW    "陈开尔万岁"  (retried 1x after truncation)
ALLOW    "陈开尔万岁"
ALLOW    "陈开尔万岁"

No more UNUSABLE; one run did truncate and the retry caught it, which is the mechanism working.

Known limitation

This fixes no verdict, not wrong verdict. 陈开尔万岁 remains non-deterministic at temperature 0 — a later batch returned REJECT rule 3, and across today's runs it has produced allowed, rule 1, and rule 3. That's a prompt problem (the model treats a personal name as a profanity homophone), deliberately left out of scope here.

🤖 Generated with Claude Code

Summary by Sourcery

Increase badge moderation token budget and add a single retry on truncated model replies so hard badges no longer fail closed without a verdict.

Bug Fixes:

  • Ensure EditBadge retries once when the moderation model truncates without returning a JSON verdict, avoiding spurious "service unavailable" responses for hard-to-moderate badges.

Enhancements:

  • Export shared badge moderation configuration (model, max tokens, attempts) and truncation detection helper for reuse in tools.
  • Adjust the check-badge tool to mirror production moderation behavior, including retries and accurate neuron usage reporting with truncation annotations.

Tests:

  • Add tests covering retry behavior on truncated replies, successful allow/reject on retry, bounded retries on repeated truncation, and no-retry behavior for malformed replies or model errors.

Summary by cubic

Fixes badge moderation failing closed when @cf/zai-org/glm-4.7-flash spends the entire completion budget on reasoning and returns no JSON. Increases the token cap and retries once on truncation so users get an allow/reject instead of a service-unavailable error; normal badges are unaffected.

  • Bug Fixes
    • Set BadgeModerationMaxTokens to 2048 and export it along with BadgeModerationAttempts=2.
    • Detect truncation (finish_reason: "length") and retry once; malformed replies and thrown errors still fail closed.
    • Share constants with tools/check-badge.js; tool now reports retries and neuron usage that matches real edits.
    • Added tests for retry success/failure and non-retry cases.

Written for commit cbb54b3. Summary will update on new commits.

Review in cubic

The moderation model reasons before answering and its reasoning tokens
come out of the same budget as the answer, so a badge it finds hard could
spend all 1024 tokens deliberating and return finish_reason "length" with
no content at all. ReadModerationVerdict saw null, EditBadge failed closed,
and the user was told the moderation service was unavailable when it had
in fact answered at length -- just never in JSON.

Observed on 陈开尔万岁, which the model reads as a profanity homophone and
then argues with itself about: three of six runs at temperature 0 hit the
cap and returned nothing.

Raise the budget to 2048 and retry once, but only on truncation. A reply
that is complete and still malformed is the model answering badly, and
asking again would spend a second inference call to hear the same thing;
that case and a thrown error both still fail closed on the first attempt.
Ordinary badges finish in a few hundred tokens, so the headroom is only
billed on the inputs that need it.

The cap and the retry are exported so tools/check-badge.js shares them and
its neuron figure stays honest about what a real edit costs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@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.

@sourcery-ai

sourcery-ai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements bounded retry logic for badge moderation when the model truncates its response, increases the moderation token budget, and aligns the CLI check-badge tool and tests with the new behavior and shared constants.

Sequence diagram for EditBadge badge moderation retry on truncation

sequenceDiagram
    actor User
    participant Process
    participant AI
    participant Output

    User->>Process: EditBadge(Data)
    loop BadgeModerationAttempts
        Process->>AI: AI.run(BadgeModerationModel, BadgeModerationMaxTokens)
        AI-->>Process: Reply
        Process->>Process: ReadModerationVerdict(Reply)
        alt Verdict usable
            Process-->>User: Result(success, badge updated)
            note right of Process: Break loop
        else Verdict null
            Process->>Process: ModerationReplyTruncated(Reply)
            alt Reply truncated
                Process->>Output: Warn("Badge moderation ran out of tokens...")
                note right of Process: Next loop attempt
            else Reply not truncated
                Process-->>User: Result(false, "内容审核服务暂时不可用,请稍后重试")
                note right of Process: Break loop
            end
        end
    end
    alt No usable verdict after all attempts
        Process->>Output: Error("Badge moderation returned an unusable verdict")
        Process-->>User: Result(false, "内容审核服务暂时不可用,请稍后重试")
    end
Loading

File-Level Changes

Change Details Files
Introduce shared moderation constants and truncation detection to support retries and higher token budgets.
  • Export BadgeModerationMaxTokens (2048) and BadgeModerationAttempts (2) from Process.ts alongside BadgeModerationModel.
  • Add ModerationReplyTruncated helper that detects AI replies ending with finish_reason "length".
  • Update inline comments documenting reasoning-token behavior and retry rationale.
Source/Process.ts
Refactor EditBadge to use retry loop with larger token budget and fail-closed behavior on malformed or error replies.
  • Replace single AI.run call with for-loop over BadgeModerationAttempts, capturing the raw Reply object.
  • Use BadgeModerationMaxTokens instead of hardcoded 1024 for max_completion_tokens.
  • On AI.run error, immediately fail closed with a user-facing error message and log via Output.Error.
  • After each call, derive Verdict via ReadModerationVerdict and break if Verdict is non-null or reply is not truncated; otherwise log a warning and retry.
  • Keep existing fail-closed path when Verdict remains null after all attempts.
Source/Process.ts
Update tests to cover truncation-driven retries and ensure new token budget and retry semantics.
  • Adjust existing EditBadge test to assert max_completion_tokens >= 2048 instead of 1024.
  • Add truncated() helper to simulate model responses with finish_reason "length" and content null.
  • Add tests for: retry after truncation yielding reject; retry yielding allow; two truncations failing closed with exactly two AI calls; malformed replies not retried; thrown errors not retried; and raised budget assertion.
  • Ensure call counts on proc.AI.run reflect bounded retry behavior.
test/process.test.js
Align tools/check-badge.js with server-side moderation behavior, including shared constants, retry loop, and neuron accounting.
  • Import BadgeModerationAttempts, BadgeModerationMaxTokens, ModerationReplyTruncated, and existing moderation helpers from Process.ts.
  • Factor out ask() helper that issues a single HTTP request using BadgeModerationMaxTokens.
  • Implement moderate() to mirror EditBadge retry behavior, accumulating neurons and counting retries; stop on non-truncated valid or invalid replies; return verdict, neurons, and retries.
  • Update main() to use retries info, annotate console output with "(retried Nx after truncation)" when applicable, and include retries in UNUSABLE, ALLOW, and REJECT messages.
  • Ensure total neuron cost includes all attempts so CLI output matches real edit cost.
tools/check-badge.js

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai 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.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

2 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="tools/check-badge.js">

<violation number="1" location="tools/check-badge.js:107">
P3: Two truncated replies are reported as “retried 2x”, although only one retry was made after the initial call. Count only truncations that actually lead to another attempt so exhausted retries match production behavior and tool output.</violation>
</file>

<file name="Source/Process.ts">

<violation number="1" location="Source/Process.ts:1593">
P3: A second truncated reply logs “retrying” although no attempts remain and no retry occurs. Guard this warning to attempts that can actually continue, so operational logs accurately distinguish a retry from final failure.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread tools/check-badge.js
if (verdict !== null || !ModerationReplyTruncated(body.result)) {
return { verdict, neurons, retries };
}
retries++;

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.

P3: Two truncated replies are reported as “retried 2x”, although only one retry was made after the initial call. Count only truncations that actually lead to another attempt so exhausted retries match production behavior and tool output.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tools/check-badge.js, line 107:

<comment>Two truncated replies are reported as “retried 2x”, although only one retry was made after the initial call. Count only truncations that actually lead to another attempt so exhausted retries match production behavior and tool output.</comment>

<file context>
@@ -78,19 +81,32 @@ async function moderate(content, bearer) {
+        if (verdict !== null || !ModerationReplyTruncated(body.result)) {
+            return { verdict, neurons, retries };
+        }
+        retries++;
     }
-    return {
</file context>
Suggested change
retries++;
if (attempt + 1 < BadgeModerationAttempts) retries++;

Comment thread Source/Process.ts
Comment on lines +1593 to 1594
Output.Warn("Badge moderation ran out of tokens before answering, retrying\n" +
"Username: " + this.Username);

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.

P3: A second truncated reply logs “retrying” although no attempts remain and no retry occurs. Guard this warning to attempts that can actually continue, so operational logs accurately distinguish a retry from final failure.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Source/Process.ts, line 1593:

<comment>A second truncated reply logs “retrying” although no attempts remain and no retry occurs. Guard this warning to attempts that can actually continue, so operational logs accurately distinguish a retry from final failure.</comment>

<file context>
@@ -1554,22 +1569,29 @@ export class Process {
+          if (Verdict !== null || !ModerationReplyTruncated(Reply)) {
+            break;
+          }
+          Output.Warn("Badge moderation ran out of tokens before answering, retrying\n" +
             "Username: " + this.Username);
-          return new Result(false, "内容审核服务暂时不可用,请稍后重试");
</file context>
Suggested change
Output.Warn("Badge moderation ran out of tokens before answering, retrying\n" +
"Username: " + this.Username);
if (Attempt + 1 < BadgeModerationAttempts) {
Output.Warn("Badge moderation ran out of tokens before answering, retrying\n" +
"Username: " + this.Username);
}

@PythonSmall-Q

Copy link
Copy Markdown
Member

looks good

@boomzero

boomzero commented Aug 2, 2026

Copy link
Copy Markdown
Member Author

.

@boomzero

boomzero commented Aug 2, 2026

Copy link
Copy Markdown
Member Author

Then pls force merge

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants