Skip to content

fix: run reactive rectifiers during streaming hedge#945

Merged
ding113 merged 2 commits into
devfrom
fix-streaming-hedge-reactive-rectifiers
Mar 18, 2026
Merged

fix: run reactive rectifiers during streaming hedge#945
ding113 merged 2 commits into
devfrom
fix-streaming-hedge-reactive-rectifiers

Conversation

@ding113

@ding113 ding113 commented Mar 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • Reuse Anthropic reactive rectifier logic for both standard retries and streaming hedge attempts
  • Allow hedge attempts to retry the same provider after thinking signature or thinking budget rectification
  • Persist rectifier audit entries even when the rectified hedge attempt runs in a shadow session

Problem

When streaming requests used the hedge-based provider race (introduced in #894), reactive rectifiers (thinking signature rectifier and thinking budget rectifier from #576) were not being applied. If a hedge attempt encountered a rectifiable error (e.g., invalid thinking signature or budget_tokens < 1024), the attempt would simply fail instead of applying the rectifier and retrying with the same provider.

Related PRs:

Solution

Extract the reactive rectifier logic into a reusable tryApplyReactiveAnthropicRectifier function that can be called from both:

  1. The standard retry loop (non-streaming path)
  2. The streaming hedge handleAttemptFailure handler

Key design decisions:

  • Each hedge attempt tracks its own reactiveRectifierRetryState to prevent infinite retry loops
  • Rectifier audit entries are persisted to the main session even when the rectified attempt runs in a shadow session (using persistSession parameter)
  • Refactored duplicate code into helper functions: addSpecialSettingForPersistence, buildRetryFailedChainEntry, getReactiveRectifierDisplayName

Changes

Core Changes (src/app/v1/_lib/proxy/forwarder.ts)

  • Added ReactiveRectifierRetryState and ReactiveRectifierResult types for tracking rectifier state
  • Created tryApplyReactiveAnthropicRectifier() function handling both signature and budget rectifiers
  • Extended StreamingHedgeAttempt with baseUrl, requestAttemptCount, and reactiveRectifierRetryState fields
  • Added reactive rectifier handling to handleAttemptFailure() in streaming hedge path
  • Created runAttempt() helper for re-executing hedge attempts after rectification
  • Refactored inline code into addSpecialSettingForPersistence() and buildRetryFailedChainEntry() helpers

Test Coverage (tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts)

  • Test: hedge alternative provider hits thinking signature error, rectifies, retries same provider with audit persistence
  • Test: hedge primary provider hits thinking budget error, rectifies, retries same provider

Testing

  • bun run build
  • bun run typecheck
  • bun run test
  • bun run lint (fails on pre-existing repository issues: Biome schema mismatch and existing unrelated lint findings)

Description enhanced by Claude AI

Greptile Summary

This PR extends the streaming hedge provider race (from #894) to run reactive Anthropic rectifiers (thinking-signature and thinking-budget rectifiers from #576) when a hedge attempt encounters a rectifiable 4xx error, instead of simply failing. The core approach — extracting tryApplyReactiveAnthropicRectifier into a shared helper called from both the standard retry loop and handleAttemptFailure, giving each hedge attempt its own reactiveRectifierRetryState, and persisting audit settings to the main session from a shadow session — is well-designed and consistent with the rest of the forwarder.

Key changes and observations:

  • tryApplyReactiveAnthropicRectifier correctly guards against infinite loops via per-attempt thinkingSignatureRetried / thinkingBudgetRetried flags.
  • The threshold timer is cleared before runAttempt is called for a rectifier retry, preventing spurious hedge_triggered audit entries (the scenario raised in a previous thread is addressed in this code).
  • mergeHedgeAttemptIntoSession now deduplicates specialSettings by JSON.stringify key rather than overwriting, so audit entries pre-persisted to the main session are not lost.
  • handleAttemptFailure invokes two await calls (categorizeErrorAsync and tryApplyReactiveAnthropicRectifier) before reaching runAttempt. There is no re-check of settled || winnerCommitted || attempt.settled at that point, meaning a winner committed during either async gap will still trigger a redundant upstream provider request.
  • The .catch() handler inside runAttempt is still missing attempt.settled in its early-return guard (previously flagged thread), allowing handleAttemptFailure to be entered for an already-settled attempt if settled/winnerCommitted happen to be false at that moment — though handleAttemptFailure's own entry guard will catch it.
  • Two new test cases verify both rectifier types in the hedge path with realistic timer progression and audit-persistence assertions.

Confidence Score: 3/5

  • The PR is functionally sound but has an open logic issue and unresolved thread items that should be addressed before merging.
  • The design and timer-handling are correct, and tests adequately cover the new paths. However, the missing guard before runAttempt after two async awaits can cause redundant upstream requests when another hedge attempt wins concurrently, the .catch() guard in runAttempt still omits attempt.settled (flagged in a previous thread), and the attemptNumber field in the non-rectifier failure chain entry still uses attempt.sequence rather than attempt.requestAttemptCount (also previously flagged). These three issues together hold the score below 4.
  • src/app/v1/_lib/proxy/forwarder.ts — specifically handleAttemptFailure (missing re-guard before runAttempt) and runAttempt's .catch() handler (missing attempt.settled check).

Important Files Changed

Filename Overview
src/app/v1/_lib/proxy/forwarder.ts Introduces reactive rectifier support for the streaming hedge path: new ReactiveRectifierRetryState/ReactiveRectifierResult types, tryApplyReactiveAnthropicRectifier helper, runAttempt refactor, and handleAttemptFailure extension. The timer-clearing and dedup logic look correct, but handleAttemptFailure is missing a re-guard before the runAttempt call after two async awaits, and the .catch() handler in runAttempt is still missing attempt.settled in its early-return guard (flagged in previous threads).
tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts Adds two new test cases covering the thinking-signature rectifier and thinking-budget rectifier in the hedge path. Timer progression, doForward call counts, rectified message verification, and storeSessionSpecialSettings audit assertions are all well-structured and align with the expected timing model.

Sequence Diagram

sequenceDiagram
    participant C as Client
    participant PF as ProxyForwarder
    participant P1 as Provider 1 (primary)
    participant P2 as Provider 2 (hedge)

    C->>PF: send(session)
    PF->>P1: runAttempt(attempt1) [doForward]
    Note over PF,P1: firstByteTimeoutMs timer starts

    Note over PF: t = firstByteTimeoutMs<br/>threshold timer fires
    PF->>P2: launchAlternative → runAttempt(attempt2) [doForward]

    P2-->>PF: ERROR: thinking signature / budget error (4xx)
    PF->>PF: handleAttemptFailure(attempt2, error)
    Note over PF: await categorizeErrorAsync()
    Note over PF: await tryApplyReactiveAnthropicRectifier()<br/>↳ mutates attempt2.session.request.message<br/>↳ persists audit to main session<br/>↳ sets reactiveRectifierRetryState flag
    PF->>PF: clearThresholdTimer(attempt2)
    Note over PF: ⚠️ no re-guard for settled/winnerCommitted here
    PF->>P2: runAttempt(attempt2) [retry, requestAttemptCount=2]

    par P1 still running
        P1-->>PF: (slow / first-byte delayed)
    and P2 rectified retry
        P2-->>PF: SUCCESS (first chunk)
    end

    PF->>PF: commitWinner(attempt2)
    PF->>PF: syncWinningAttemptSession → mergeSpecialSettings (dedup)
    PF->>PF: abortAllAttempts (cancels P1)
    PF-->>C: streaming response from P2 (rectified)
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/forwarder.ts
Line: 3195-3256

Comment:
**Stale guard before `runAttempt` after two async awaits**

`handleAttemptFailure` re-checks `settled || winnerCommitted || attempt.settled` only at the very top (line 3154). However, two `await` calls follow before `runAttempt(attempt)` is reached — `categorizeErrorAsync` (line 3158) and `tryApplyReactiveAnthropicRectifier` (line 3195, which itself awaits `getCachedSystemSettings`). During either of these async gaps another hedge attempt could win and set `winnerCommitted = true`.

As a result, `runAttempt(attempt)` at line 3256 is called even though a winner has already been committed. The new provider request cannot be cancelled until the response arrives (when the `.then()` guard at line 3095 finally catches it). This wastes a full round-trip to the upstream provider.

A re-guard immediately before the retry branch would prevent the redundant call:

```suggestion
          if (settled || winnerCommitted || attempt.settled) {
            return;
          }

          if (attempt.thresholdTimer) {
            clearTimeout(attempt.thresholdTimer);
            attempt.thresholdTimer = null;
          }
          attempt.requestAttemptCount += 1;
          runAttempt(attempt);
          return;
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "fix: address hedge r..."

Greptile also left 1 inline comment on this PR.

@coderabbitai

coderabbitai Bot commented Mar 18, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

在代理转发器中添加了面向Anthropic提供商的“响应式整流器(ReactiveRectifier)”重试流程:新增内部重试状态与结果类型、整流应用函数与持久化辅助、对冲(hedge)尝试数据扩展及重构的尝试生命周期管理;相关测试补充了思维署名与思维预算整流+重试场景。

Changes

Cohort / File(s) Summary
响应式整流器核心实现
src/app/v1/_lib/proxy/forwarder.ts
新增内部类型(ReactiveRectifierRetryState、ReactiveRectifierResult)、函数(tryApplyReactiveAnthropicRectifier、getReactiveRectifierDisplayName、buildRetryFailedChainEntry、addSpecialSettingForPersistence 等)及持久化辅助,实现Anthropic thinking_signature/budget的“整流+重试”流;扩展 StreamingHedgeAttempt 以携带 per-try 数据(baseUrl、requestAttemptCount、reactiveRectifierRetryState);用 runAttempt 抽取并重构每次尝试的生命周期逻辑。
测试:对冲首字节场景扩展
tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts
在测试环境中暴露 getCachedSystemSettings 并新增 withThinkingBlocks 测试辅助;新增两条测试路径验证在 alternate provider 触发的 thinking_signature 和 thinking_budget 错误会触发整流、持久化设置并对同一提供商重试,同时记录相应审计链条条目。
其他/声明变更说明
package.json
清单变动(已包含在diff摘要),无公共 API 导出改动;新增类型与辅助函数均为模块内部实现,不对外暴露。

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 标题准确总结了主要变化:在流式对冲过程中运行响应式修正器,这是PR的核心功能。
Description check ✅ Passed PR描述与变更集相关,清晰阐述了问题、解决方案和关键设计决策,详细说明了核心变更和测试覆盖。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-streaming-hedge-reactive-rectifiers
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the robustness of the proxy forwarder by integrating Anthropic reactive rectifier logic into streaming hedge attempts. This allows the system to intelligently modify problematic requests and retry them with the same provider, improving success rates for streaming requests that encounter specific Anthropic errors. The changes also ensure that all rectification actions are properly audited, providing better visibility into the system's retry mechanisms.

Highlights

  • Unified Reactive Rectifier Logic: The logic for Anthropic reactive rectifiers (thinking signature and thinking budget) has been consolidated into a single, reusable function, tryApplyReactiveAnthropicRectifier, reducing code duplication and improving maintainability for both standard retries and streaming hedge attempts.
  • Reactive Rectifiers for Streaming Hedge: Reactive rectifiers are now actively applied during streaming hedge attempts. If a rectifier successfully modifies the request (e.g., for thinking signature or budget issues), the hedge attempt will retry with the same provider, preventing unnecessary provider switching.
  • Persistent Rectifier Audit Entries: Audit entries for reactive rectifiers are now consistently persisted, even when the rectified hedge attempt runs within a shadow session, ensuring comprehensive logging of rectification actions.
  • Enhanced Streaming Hedge Attempt State: The StreamingHedgeAttempt type has been extended to include baseUrl, requestAttemptCount, and reactiveRectifierRetryState, allowing for more granular control and tracking of individual hedge attempts and their rectification status.
  • Refactored Error Handling and Retry Logging: Error handling within the ProxyForwarder has been refactored, including a new buildRetryFailedChainEntry function to standardize the logging of failed retry attempts, and the runAttempt function has been extracted for better modularity in streaming hedge logic.
  • Comprehensive Unit Tests: New unit tests have been added to specifically cover scenarios where reactive rectifiers are triggered and applied during streaming hedge attempts, verifying correct behavior for both thinking signature and thinking budget rectifications.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions github-actions Bot added bug Something isn't working area:provider size/XL Extra Large PR (> 1000 lines) labels Mar 18, 2026

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request effectively refactors the reactive rectifier logic for Anthropic providers and extends its functionality to streaming hedge attempts. The code is now more modular and less repetitive, especially with the introduction of helper functions like tryApplyReactiveAnthropicRectifier and buildRetryFailedChainEntry. My main feedback is a suggestion to further reduce duplication within the new tryApplyReactiveAnthropicRectifier function for improved maintainability.

Comment on lines +450 to +592
async function tryApplyReactiveAnthropicRectifier(params: {
provider: Provider;
requestSession: ProxySession;
persistSession: ProxySession;
errorMessage: string;
attemptNumber: number;
retryAttemptNumber: number;
retryState: ReactiveRectifierRetryState;
}): Promise<ReactiveRectifierResult> {
const {
provider,
requestSession,
persistSession,
errorMessage,
attemptNumber,
retryAttemptNumber,
} = params;
const isAnthropicProvider =
provider.providerType === "claude" || provider.providerType === "claude-auth";

if (!isAnthropicProvider) {
return { matched: false };
}

const signatureTrigger = detectThinkingSignatureRectifierTrigger(errorMessage);
if (signatureTrigger) {
const settings = await getCachedSystemSettings();
const enabled = settings.enableThinkingSignatureRectifier ?? true;

if (!enabled) {
return { matched: false };
}

if (params.retryState.thinkingSignatureRetried) {
return {
matched: true,
applied: false,
reason: "already_retried",
rectifierType: "thinking_signature_rectifier",
trigger: signatureTrigger,
};
}

const requestDetailsBeforeRectify = buildRequestDetails(requestSession);
const rectified = rectifyAnthropicRequestMessage(
requestSession.request.message as Record<string, unknown>
);

addSpecialSettingForPersistence(requestSession, persistSession, {
type: "thinking_signature_rectifier",
scope: "request",
hit: rectified.applied,
providerId: provider.id,
providerName: provider.name,
trigger: signatureTrigger,
attemptNumber,
retryAttemptNumber,
removedThinkingBlocks: rectified.removedThinkingBlocks,
removedRedactedThinkingBlocks: rectified.removedRedactedThinkingBlocks,
removedSignatureFields: rectified.removedSignatureFields,
});
await persistSpecialSettings(persistSession);

if (!rectified.applied) {
return {
matched: true,
applied: false,
reason: "not_applicable",
rectifierType: "thinking_signature_rectifier",
trigger: signatureTrigger,
};
}

params.retryState.thinkingSignatureRetried = true;
return {
matched: true,
applied: true,
rectifierType: "thinking_signature_rectifier",
trigger: signatureTrigger,
requestDetailsBeforeRectify,
};
}

const budgetTrigger = detectThinkingBudgetRectifierTrigger(errorMessage);
if (!budgetTrigger) {
return { matched: false };
}

const settings = await getCachedSystemSettings();
const enabled = settings.enableThinkingBudgetRectifier ?? true;

if (!enabled) {
return { matched: false };
}

if (params.retryState.thinkingBudgetRetried) {
return {
matched: true,
applied: false,
reason: "already_retried",
rectifierType: "thinking_budget_rectifier",
trigger: budgetTrigger,
};
}

const requestDetailsBeforeRectify = buildRequestDetails(requestSession);
const rectified = rectifyThinkingBudget(
requestSession.request.message as Record<string, unknown>
);

addSpecialSettingForPersistence(requestSession, persistSession, {
type: "thinking_budget_rectifier",
scope: "request",
hit: rectified.applied,
providerId: provider.id,
providerName: provider.name,
trigger: budgetTrigger,
attemptNumber,
retryAttemptNumber,
before: rectified.before,
after: rectified.after,
});
await persistSpecialSettings(persistSession);

if (!rectified.applied) {
return {
matched: true,
applied: false,
reason: "not_applicable",
rectifierType: "thinking_budget_rectifier",
trigger: budgetTrigger,
};
}

params.retryState.thinkingBudgetRetried = true;
return {
matched: true,
applied: true,
rectifierType: "thinking_budget_rectifier",
trigger: budgetTrigger,
requestDetailsBeforeRectify,
};
}

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.

medium

This function is a great step towards centralizing the rectifier logic. However, there's significant code duplication between the handling of signatureTrigger and budgetTrigger. The logic for checking settings, retries, applying rectification, and persisting audit entries is nearly identical in both blocks.

To improve maintainability and reduce code duplication, consider refactoring this into a more generic helper function. This helper could take rectifier-specific logic as arguments, such as:

  • The trigger detection function (detectThinkingSignatureRectifierTrigger or detectThinkingBudgetRectifierTrigger)
  • The setting key to check for enablement (enableThinkingSignatureRectifier or enableThinkingBudgetRectifier)
  • The retry state property to check and update (thinkingSignatureRetried or thinkingBudgetRetried)
  • The rectification function (rectifyAnthropicRequestMessage or rectifyThinkingBudget)
  • A function to build the specific SpecialSetting object for auditing.

This would make tryApplyReactiveAnthropicRectifier much more concise and easier to extend with new rectifiers in the future.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/v1/_lib/proxy/forwarder.ts`:
- Around line 3224-3254: When a reactive rectifier triggers a retry the code
increments attempt.requestAttemptCount and calls runAttempt(attempt) then
returns, but it skips clearing the threshold timer so the original
thresholdTimer can still fire and incorrectly trigger hedging; before calling
runAttempt(attempt) (inside the reactive rectifier branch handling in
ProxyForwarder) clear/reset attempt.thresholdTimer (e.g. clearTimeout and set to
null) so the timer cannot fire during the retry, then call runAttempt(attempt)
and return; reference symbols: attempt, attempt.requestAttemptCount,
attempt.thresholdTimer, runAttempt.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f0200a78-b82b-4748-8643-0fd0e950562a

📥 Commits

Reviewing files that changed from the base of the PR and between a47c7e1 and 728a98c.

📒 Files selected for processing (2)
  • src/app/v1/_lib/proxy/forwarder.ts
  • tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts

Comment thread src/app/v1/_lib/proxy/forwarder.ts
Comment on lines +3251 to +3253
attempt.requestAttemptCount += 1;
runAttempt(attempt);
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Threshold timer not cleared before rectifier retry

When tryApplyReactiveAnthropicRectifier returns applied: true, the function calls runAttempt(attempt) and returns early — but attempt.thresholdTimer is never cleared. The hedge threshold timer was started at startAttempt time and fires at launchTime + firstByteTimeoutMs, regardless of any mid-flight rectifier retry.

This causes two problems:

  1. Misleading audit entry: When the timer fires during the retry, it still adds a "hedge_triggered" chain entry (reason: "hedge_triggered") even though the provider never actually timed out — it returned a 400 error that was rectified and retried. This is confirmed by the test: in the signature-error test, provider2's timer fires at t = 200ms (100ms after p2 launched) while the rectifier retry is in progress (t = 150ms → 330ms), producing a spurious "hedge_triggered" audit record.

  2. Unnecessary launchAlternative() call: The timer fires and calls launchAlternative(). In the test this is masked because the mock is exhausted and returns null (setting noMoreProviders = true), but in production with a real provider pool a third provider would be launched unnecessarily while the rectifier retry is already in progress.

The fix should clear (and optionally re-arm) the threshold timer before launching the retry:

// Clear the stale threshold timer before re-launching
if (attempt.thresholdTimer) {
  clearTimeout(attempt.thresholdTimer);
  attempt.thresholdTimer = null;
}
// Optionally re-arm a fresh timer for the retry here

attempt.requestAttemptCount += 1;
runAttempt(attempt);
return;
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/forwarder.ts
Line: 3251-3253

Comment:
**Threshold timer not cleared before rectifier retry**

When `tryApplyReactiveAnthropicRectifier` returns `applied: true`, the function calls `runAttempt(attempt)` and returns early — but `attempt.thresholdTimer` is never cleared. The hedge threshold timer was started at `startAttempt` time and fires at `launchTime + firstByteTimeoutMs`, regardless of any mid-flight rectifier retry.

This causes two problems:

1. **Misleading audit entry**: When the timer fires during the retry, it still adds a `"hedge_triggered"` chain entry (`reason: "hedge_triggered"`) even though the provider never actually timed out — it returned a 400 error that was rectified and retried. This is confirmed by the test: in the signature-error test, provider2's timer fires at `t = 200ms` (100ms after p2 launched) while the rectifier retry is in progress (`t = 150ms → 330ms`), producing a spurious `"hedge_triggered"` audit record.

2. **Unnecessary `launchAlternative()` call**: The timer fires and calls `launchAlternative()`. In the test this is masked because the mock is exhausted and returns `null` (setting `noMoreProviders = true`), but in production with a real provider pool a third provider would be launched unnecessarily while the rectifier retry is already in progress.

The fix should clear (and optionally re-arm) the threshold timer before launching the retry:

```typescript
// Clear the stale threshold timer before re-launching
if (attempt.thresholdTimer) {
  clearTimeout(attempt.thresholdTimer);
  attempt.thresholdTimer = null;
}
// Optionally re-arm a fresh timer for the retry here

attempt.requestAttemptCount += 1;
runAttempt(attempt);
return;
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/app/v1/_lib/proxy/forwarder.ts Outdated
Comment on lines +3094 to +3095
.then(async (response) => {
if (settled || winnerCommitted) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Missing attempt.settled check in early-cancellation guard

The guard at the top of the .then() block checks settled || winnerCommitted but not attempt.settled. In contrast, commitWinner (line 3293) checks all three: settled || winnerCommitted || attempt.settled.

If attempt.settled becomes true between when runAttempt was called (for a rectifier retry) and when the response arrives (e.g., the threshold timer fires and drives the attempt through a separate handleAttemptFailure path that sets attempt.settled = true), the response body will not be cancelled in this guard. The code will proceed to set attempt.response, acquire a reader, and only then be blocked by commitWinner's guard — leaving the response body open unnecessarily.

Suggested change
.then(async (response) => {
if (settled || winnerCommitted) {
if (settled || winnerCommitted || attempt.settled) {
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/forwarder.ts
Line: 3094-3095

Comment:
**Missing `attempt.settled` check in early-cancellation guard**

The guard at the top of the `.then()` block checks `settled || winnerCommitted` but not `attempt.settled`. In contrast, `commitWinner` (line 3293) checks all three: `settled || winnerCommitted || attempt.settled`.

If `attempt.settled` becomes `true` between when `runAttempt` was called (for a rectifier retry) and when the response arrives (e.g., the threshold timer fires and drives the attempt through a separate `handleAttemptFailure` path that sets `attempt.settled = true`), the response body will not be cancelled in this guard. The code will proceed to set `attempt.response`, acquire a reader, and only then be blocked by `commitWinner`'s guard — leaving the response body open unnecessarily.

```suggestion
          if (settled || winnerCommitted || attempt.settled) {
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +3241 to +3248
buildRetryFailedChainEntry(
attempt.provider,
attempt.endpointAudit,
attempt.sequence,
error,
errorMessage,
reactiveRectifierResult.requestDetailsBeforeRectify
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 attempt.sequence used as attemptNumber in retry chain entry

buildRetryFailedChainEntry is called with attempt.sequence (the hedge participant index: 1, 2, 3, …) as attemptNumber. In the standard (non-hedge) retry path the same helper is called with attemptCount (also 1, 2, 3, … but measuring how many times this provider has been tried). These two counters mean different things: sequence reflects the hedge slot, while the standard path's attemptCount reflects the retry iteration.

For the hedge rectifier case the chain entry will always show attemptNumber = attempt.sequence (e.g. 2 for the second hedge participant), regardless of whether this is the first or a subsequent rectifier retry. This is inconsistent with the standard path and could make audit data harder to interpret. Consider using attempt.requestAttemptCount here, which tracks per-attempt retries and is already incremented just below.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/forwarder.ts
Line: 3241-3248

Comment:
**`attempt.sequence` used as `attemptNumber` in retry chain entry**

`buildRetryFailedChainEntry` is called with `attempt.sequence` (the hedge participant index: 1, 2, 3, …) as `attemptNumber`. In the standard (non-hedge) retry path the same helper is called with `attemptCount` (also 1, 2, 3, … but measuring how many times this provider has been tried). These two counters mean different things: `sequence` reflects the hedge slot, while the standard path's `attemptCount` reflects the retry iteration.

For the hedge rectifier case the chain entry will always show `attemptNumber = attempt.sequence` (e.g. `2` for the second hedge participant), regardless of whether this is the first or a subsequent rectifier retry. This is inconsistent with the standard path and could make audit data harder to interpret. Consider using `attempt.requestAttemptCount` here, which tracks per-attempt retries and is already incremented just below.

How can I resolve this? If you propose a fix, please make it concise.

@github-actions github-actions Bot added the size/L Large PR (< 1000 lines) label Mar 18, 2026

@github-actions github-actions 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.

Code Review Summary

This PR refactors reactive rectifier logic for streaming hedge scenarios. While the overall refactoring is sound, there is a critical threshold-timer lifetime bug that can produce incorrect audit data and trigger unnecessary provider launches in production.

PR Size: L

  • Lines changed: 1028 (687 additions, 341 deletions)
  • Files changed: 2

Note: The PR is labeled size/XL but with only 2 changed files, this is a focused change that doesn't require splitting.

Issues Found

Category Critical High Medium Low
Logic/Bugs 1 0 1 1
Security 0 0 0 0
Error Handling 0 0 0 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

Critical Issues (Must Fix)

[LOGIC-BUG] Threshold timer not cleared before rectifier retry

When tryApplyReactiveAnthropicRectifier returns applied: true, the function calls runAttempt(attempt) and returns early — but attempt.thresholdTimer is never cleared. The hedge threshold timer was started at startAttempt time and fires at launchTime + firstByteTimeoutMs, regardless of any mid-flight rectifier retry.

This causes two problems:

  1. Misleading audit entry: When the timer fires during the retry, it adds a spurious "hedge_triggered" chain entry even though the provider returned a rectifiable error (not a timeout).
  2. Unnecessary provider launch: The timer calls launchAlternative(), which can launch a third provider unnecessarily while the rectifier retry is already in progress.

Location: src/app/v1/_lib/proxy/forwarder.ts lines 3251-3253

Suggested fix:

// Clear the stale threshold timer before re-launching
if (attempt.thresholdTimer) {
  clearTimeout(attempt.thresholdTimer);
  attempt.thresholdTimer = null;
}

attempt.requestAttemptCount += 1;
runAttempt(attempt);
return;

Medium Priority Issues (Should Fix)

[LOGIC-BUG] Missing attempt.settled check in early-cancellation guard

The guard at the top of the .then() block (line 3095) checks settled || winnerCommitted but not attempt.settled. In contrast, commitWinner (line 3293) checks all three: settled || winnerCommitted || attempt.settled.

If attempt.settled becomes true between when runAttempt was called and when the response arrives (e.g., the threshold timer fires and drives the attempt through a separate handleAttemptFailure path), the response body will not be cancelled in this guard. The code will proceed to set attempt.response, acquire a reader, and only then be blocked by commitWinner's guard — leaving the response body open unnecessarily.

Suggested fix:

if (settled || winnerCommitted || attempt.settled) {

Low Priority Issues (Author's Discretion)

[NAMING-POOR] attempt.sequence used as attemptNumber in retry chain entry

buildRetryFailedChainEntry is called with attempt.sequence (the hedge participant index: 1, 2, 3...) as attemptNumber. In the standard (non-hedge) retry path, the same helper is called with attemptCount (retry iteration). These mean different things.

For hedge rectifier retries, the chain entry will always show attemptNumber = attempt.sequence regardless of whether this is the first or a subsequent rectifier retry. Consider using attempt.requestAttemptCount which tracks per-attempt retries.

Location: Line 3244

Review Coverage

  • Logic and correctness - Issues identified (see above)
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - Adequate (2 new tests for rectifier retry scenarios)
  • Code clarity - Good

Notes

  • The Greptile summary correctly identified the threshold-timer issue
  • Test coverage for the new rectifier retry scenarios is present but doesn't assert on the spurious "hedge_triggered" audit entry
  • The extraction of helper functions (addSpecialSettingForPersistence, buildRetryFailedChainEntry, getReactiveRectifierDisplayName) improves code maintainability

Automated review by Claude AI

): void {
ownerSession.addSpecialSetting(setting);
if (persistSession !== ownerSession) {
persistSession.addSpecialSetting(setting);

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.

[HIGH] [LOGIC-BUG] specialSettings can be dropped when a different shadow session wins the hedge

Why this is a problem: addSpecialSettingForPersistence() writes rectifier audit entries into the main session when persistSession !== ownerSession (see src/app/v1/_lib/proxy/forwarder.ts:388-390: persistSession.addSpecialSetting(setting);). But if a different shadow attempt later wins, syncWinningAttemptSession() overwrites the target session’s specialSettings from the winner snapshot (see src/app/v1/_lib/proxy/forwarder.ts:3673: targetState.specialSettings = [...sourceState.specialSettings];), which can discard those previously-persisted audit entries. The streaming finalization path persists specialSettings again from the winning session (src/app/v1/_lib/proxy/response-handler.ts:2136), so this can silently erase rectifier audit data.

Suggested fix:

// src/app/v1/_lib/proxy/forwarder.ts (syncWinningAttemptSession)
const existingSpecialSettings = new Set(
  targetState.specialSettings.map((setting) => JSON.stringify(setting))
);
const mergedSpecialSettings = [...targetState.specialSettings];
for (const setting of sourceState.specialSettings) {
  const key = JSON.stringify(setting);
  if (existingSpecialSettings.has(key)) continue;
  existingSpecialSettings.add(key);
  mergedSpecialSettings.push(setting);
}
targetState.specialSettings = mergedSpecialSettings;

@github-actions github-actions 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.

Code Review Summary

This PR makes a large refactor in src/app/v1/_lib/proxy/forwarder.ts to reuse reactive rectifier logic for both standard retries and streaming first-byte hedge attempts. One high-risk correctness issue remains: rectifier audit entries added to the main session can be dropped when a different shadow session wins the hedge, causing persisted special_settings to be incomplete.

PR Size: XL

  • Lines changed: 1028
  • Files changed: 2
  • Split suggestion (XL): Split into (1) pure refactor/extraction of reactive-rectifier helpers and chain-entry builders, and (2) streaming-hedge behavior changes + tests, to reduce review surface area and isolate behavior changes.

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 1 0 0
Security 0 0 0 0
Error Handling 0 0 0 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

Critical Issues (Must Fix)

None.

High Priority Issues (Should Fix)

  • src/app/v1/_lib/proxy/forwarder.ts:388 / src/app/v1/_lib/proxy/forwarder.ts:3673 - Hedge winner sync overwrites specialSettings, which can drop rectifier audit entries previously added to the main session from other (losing) hedge attempts. Confidence: 85.

Review Coverage

  • Logic and correctness
  • Security (OWASP Top 10)
  • Error handling
  • Type safety
  • Documentation accuracy
  • Test coverage
  • Code clarity

Automated review by Codex AI

@github-actions github-actions 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.

  • Reviewed PR #945 (“fix: run reactive rectifiers during streaming hedge”); changed files: src/app/v1/_lib/proxy/forwarder.ts, tests/unit/proxy/proxy-forwarder-hedge-first-byte.test.ts.
  • PR size computed as XL (687 additions + 341 deletions = 1028 lines; 2 files) and applied label size/XL.
  • Posted 1 inline comment ([HIGH] [LOGIC-BUG]) on src/app/v1/_lib/proxy/forwarder.ts:389 about specialSettings being overwritten when a different shadow session wins the hedge, potentially dropping rectifier audit entries (discussion: https://github.com/ding113/claude-code-hub/pull/945#discussion_r2954323197).
  • Submitted the required review summary via gh pr review.

- Add attempt.settled guard in .then() callback to prevent stale response processing
- Clear thresholdTimer before rectifier retry to avoid spurious hedge triggers
- Use requestAttemptCount instead of sequence for accurate retry chain entries
- Merge specialSettings on hedge winner sync to preserve rectifier audit data

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

Comment on lines +3195 to +3256
const reactiveRectifierResult = await tryApplyReactiveAnthropicRectifier({
provider: attempt.provider,
requestSession: attempt.session,
persistSession: session,
errorMessage,
attemptNumber: attempt.requestAttemptCount,
retryAttemptNumber: attempt.requestAttemptCount + 1,
retryState: attempt.reactiveRectifierRetryState,
});

if (reactiveRectifierResult.matched) {
if (!reactiveRectifierResult.applied) {
if (reactiveRectifierResult.reason === "not_applicable") {
logger.info(
`ProxyForwarder: ${getReactiveRectifierDisplayName(
reactiveRectifierResult.rectifierType
)} not applicable in hedge, skipping retry`,
{
providerId: attempt.provider.id,
providerName: attempt.provider.name,
trigger: reactiveRectifierResult.trigger,
participantSequence: attempt.sequence,
attemptNumber: attempt.requestAttemptCount,
}
);
}

errorCategory = ErrorCategory.NON_RETRYABLE_CLIENT_ERROR;
lastErrorCategory = errorCategory;
} else {
logger.info(
`ProxyForwarder: ${getReactiveRectifierDisplayName(
reactiveRectifierResult.rectifierType
)} applied in hedge, retrying same provider`,
{
providerId: attempt.provider.id,
providerName: attempt.provider.name,
trigger: reactiveRectifierResult.trigger,
participantSequence: attempt.sequence,
attemptNumber: attempt.requestAttemptCount,
willRetryAttemptNumber: attempt.requestAttemptCount + 1,
}
);

session.addProviderToChain(
attempt.provider,
buildRetryFailedChainEntry(
attempt.provider,
attempt.endpointAudit,
attempt.requestAttemptCount,
error,
errorMessage,
reactiveRectifierResult.requestDetailsBeforeRectify
)
);

if (attempt.thresholdTimer) {
clearTimeout(attempt.thresholdTimer);
attempt.thresholdTimer = null;
}
attempt.requestAttemptCount += 1;
runAttempt(attempt);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Stale guard before runAttempt after two async awaits

handleAttemptFailure re-checks settled || winnerCommitted || attempt.settled only at the very top (line 3154). However, two await calls follow before runAttempt(attempt) is reached — categorizeErrorAsync (line 3158) and tryApplyReactiveAnthropicRectifier (line 3195, which itself awaits getCachedSystemSettings). During either of these async gaps another hedge attempt could win and set winnerCommitted = true.

As a result, runAttempt(attempt) at line 3256 is called even though a winner has already been committed. The new provider request cannot be cancelled until the response arrives (when the .then() guard at line 3095 finally catches it). This wastes a full round-trip to the upstream provider.

A re-guard immediately before the retry branch would prevent the redundant call:

Suggested change
const reactiveRectifierResult = await tryApplyReactiveAnthropicRectifier({
provider: attempt.provider,
requestSession: attempt.session,
persistSession: session,
errorMessage,
attemptNumber: attempt.requestAttemptCount,
retryAttemptNumber: attempt.requestAttemptCount + 1,
retryState: attempt.reactiveRectifierRetryState,
});
if (reactiveRectifierResult.matched) {
if (!reactiveRectifierResult.applied) {
if (reactiveRectifierResult.reason === "not_applicable") {
logger.info(
`ProxyForwarder: ${getReactiveRectifierDisplayName(
reactiveRectifierResult.rectifierType
)} not applicable in hedge, skipping retry`,
{
providerId: attempt.provider.id,
providerName: attempt.provider.name,
trigger: reactiveRectifierResult.trigger,
participantSequence: attempt.sequence,
attemptNumber: attempt.requestAttemptCount,
}
);
}
errorCategory = ErrorCategory.NON_RETRYABLE_CLIENT_ERROR;
lastErrorCategory = errorCategory;
} else {
logger.info(
`ProxyForwarder: ${getReactiveRectifierDisplayName(
reactiveRectifierResult.rectifierType
)} applied in hedge, retrying same provider`,
{
providerId: attempt.provider.id,
providerName: attempt.provider.name,
trigger: reactiveRectifierResult.trigger,
participantSequence: attempt.sequence,
attemptNumber: attempt.requestAttemptCount,
willRetryAttemptNumber: attempt.requestAttemptCount + 1,
}
);
session.addProviderToChain(
attempt.provider,
buildRetryFailedChainEntry(
attempt.provider,
attempt.endpointAudit,
attempt.requestAttemptCount,
error,
errorMessage,
reactiveRectifierResult.requestDetailsBeforeRectify
)
);
if (attempt.thresholdTimer) {
clearTimeout(attempt.thresholdTimer);
attempt.thresholdTimer = null;
}
attempt.requestAttemptCount += 1;
runAttempt(attempt);
if (settled || winnerCommitted || attempt.settled) {
return;
}
if (attempt.thresholdTimer) {
clearTimeout(attempt.thresholdTimer);
attempt.thresholdTimer = null;
}
attempt.requestAttemptCount += 1;
runAttempt(attempt);
return;
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/forwarder.ts
Line: 3195-3256

Comment:
**Stale guard before `runAttempt` after two async awaits**

`handleAttemptFailure` re-checks `settled || winnerCommitted || attempt.settled` only at the very top (line 3154). However, two `await` calls follow before `runAttempt(attempt)` is reached — `categorizeErrorAsync` (line 3158) and `tryApplyReactiveAnthropicRectifier` (line 3195, which itself awaits `getCachedSystemSettings`). During either of these async gaps another hedge attempt could win and set `winnerCommitted = true`.

As a result, `runAttempt(attempt)` at line 3256 is called even though a winner has already been committed. The new provider request cannot be cancelled until the response arrives (when the `.then()` guard at line 3095 finally catches it). This wastes a full round-trip to the upstream provider.

A re-guard immediately before the retry branch would prevent the redundant call:

```suggestion
          if (settled || winnerCommitted || attempt.settled) {
            return;
          }

          if (attempt.thresholdTimer) {
            clearTimeout(attempt.thresholdTimer);
            attempt.thresholdTimer = null;
          }
          attempt.requestAttemptCount += 1;
          runAttempt(attempt);
          return;
```

How can I resolve this? If you propose a fix, please make it concise.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/v1/_lib/proxy/forwarder.ts`:
- Around line 3082-3085: The retry path is clearing first-byte timeout by
setting attempt.provider.firstByteTimeoutStreamingMs to 0 (providerForRequest)
and then clearing thresholdTimer before calling runAttempt(attempt), which can
allow retries to hang indefinitely; preserve or restore the original first-byte
timeout and ensure the threshold timer is (re)initialized before invoking
runAttempt. Specifically, keep the original firstByteTimeoutStreamingMs value on
the attempt (or attach it as e.g. attempt._originalFirstByteTimeout), avoid
replacing it with 0 in providerForRequest (or restore it on attempt.provider
before retry), and restart the thresholdTimer prior to calling
runAttempt(attempt) so the same timeout protection applies on retries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9295ee04-de86-42cc-84de-162e361df2ea

📥 Commits

Reviewing files that changed from the base of the PR and between 728a98c and 2409eca.

📒 Files selected for processing (1)
  • src/app/v1/_lib/proxy/forwarder.ts

Comment on lines +3082 to +3085
const providerForRequest =
attempt.provider.firstByteTimeoutStreamingMs > 0
? { ...attempt.provider, firstByteTimeoutStreamingMs: 0 }
: attempt.provider;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Rectifier 重试路径可能失去超时保护并导致请求悬挂

在 Line [3083-3085] 里将 firstByteTimeoutStreamingMs 置为 0,而在 Line [3251-3257] 里重试前清掉 thresholdTimer 后直接 runAttempt(attempt),未重置阈值计时器。两者叠加后,若重试请求在首字节前卡住且没有可用备选 provider,可能长期不返回。

🔧 建议修复(示例)
-      const providerForRequest =
-        attempt.provider.firstByteTimeoutStreamingMs > 0
-          ? { ...attempt.provider, firstByteTimeoutStreamingMs: 0 }
-          : attempt.provider;
+      const providerForRequest = attempt.provider;
...
           if (attempt.thresholdTimer) {
             clearTimeout(attempt.thresholdTimer);
             attempt.thresholdTimer = null;
           }
+          attempt.thresholdTriggered = false;
+          if (attempt.firstByteTimeoutMs > 0) {
+            attempt.thresholdTimer = setTimeout(() => {
+              if (settled || attempt.settled || attempt.thresholdTriggered) return;
+              attempt.thresholdTriggered = true;
+              attempt.session.addProviderToChain(attempt.provider, {
+                ...attempt.endpointAudit,
+                reason: "hedge_triggered",
+                attemptNumber: attempt.sequence,
+                circuitState: getCircuitState(attempt.provider.id),
+              });
+              void launchAlternative();
+            }, attempt.firstByteTimeoutMs);
+          }
           attempt.requestAttemptCount += 1;
           runAttempt(attempt);
           return;

Also applies to: 3251-3257

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/proxy/forwarder.ts` around lines 3082 - 3085, The retry path
is clearing first-byte timeout by setting
attempt.provider.firstByteTimeoutStreamingMs to 0 (providerForRequest) and then
clearing thresholdTimer before calling runAttempt(attempt), which can allow
retries to hang indefinitely; preserve or restore the original first-byte
timeout and ensure the threshold timer is (re)initialized before invoking
runAttempt. Specifically, keep the original firstByteTimeoutStreamingMs value on
the attempt (or attach it as e.g. attempt._originalFirstByteTimeout), avoid
replacing it with 0 in providerForRequest (or restore it on attempt.provider
before retry), and restart the thresholdTimer prior to calling
runAttempt(attempt) so the same timeout protection applies on retries.

@ding113
ding113 merged commit 98aa821 into dev Mar 18, 2026
14 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Claude Code Hub Roadmap Mar 18, 2026
@github-actions github-actions Bot mentioned this pull request Mar 22, 2026
@ding113
ding113 deleted the fix-streaming-hedge-reactive-rectifiers branch May 13, 2026 06:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:provider bug Something isn't working size/L Large PR (< 1000 lines) size/XL Extra Large PR (> 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant