Skip to content

refactor: modularize 3.2K-line usage.ts (Issue #3594) - #3795

Closed
oyi77 wants to merge 36 commits into
diegosouzapw:release/v3.8.27from
oyi77:pr/3645-clean
Closed

oyi77 wants to merge 36 commits into
diegosouzapw:release/v3.8.27from
oyi77:pr/3645-clean

Conversation

@oyi77

@oyi77 oyi77 commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Clean re-cut of PR #3645 — modularizes open-sse/services/usage.ts (3393→633 lines) into 25 focused modules under open-sse/services/usage/.

Cherry-picked only the modularization commit (23029463e) onto current release/v3.8.24. No unrelated changes.

diegosouzapw and others added 30 commits June 12, 2026 23:52
…Error on Node.js v24 (diegosouzapw#3748) (diegosouzapw#3750)

Node.js v24 added strict type checking to process.exit() and throws
TypeError [ERR_INVALID_ARG_TYPE] when given a non-number. The spawn
'error' event passes err.code (e.g. 'ENOENT') — a string, not a number
— via `err.code ?? -1` (nullish coalescing doesn't help since 'ENOENT'
is not null/undefined). handleExit() now normalises the code to a number
at the top; the 'error' callback passes -1 unconditionally.
Adds OMNIROUTE_EMERGENCY_FALLBACK to the Feature Flags page as a runtime boolean (default on, no restart); resolves through the feature-flag stack so DB overrides can enable/disable it while preserving the raw env fallback path. Integrated into release/v3.8.24.
Make the Request Logs Clean history action a full request-history purge instead of retention-only: clears call_logs, legacy request_detail_logs, and local JSON artifacts under DATA_DIR/call_logs (including orphans), via a dedicated POST /api/settings/purge-request-history route. Also hardens the legacy purge-call-logs route to route errors through buildErrorBody (Rule #12). Integrated into release/v3.8.24.
)

Inject retrieved OmniRoute memory into Codex Responses-over-WebSocket prepare requests via the instructions field, deriving the retrieval query from the latest user Responses input while skipping tool/reasoning payloads, with a duplicate-safe injection guard. The Responses-WS request-history logging this branch also carried already landed on release; the net change here is the memory-injection parity with the SSE path. Integrated into release/v3.8.24.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
… 24h cooldown to DB (diegosouzapw#3707) (diegosouzapw#3754)

Three bugs fixed together:
1. decide429() was called AFTER the Google One AI credits retry — a quota-exhausted
   account could hang ~41s on the credits HTTP call before the full_quota_exhausted
   verdict was ever computed. Now decide429() runs first; credits retry is skipped
   when kind === full_quota_exhausted.
2. setConnectionRateLimitUntil() was never called from the antigravity executor —
   the 24h cooldown state lived only in memory and was lost on restart, causing
   post-restart requests to re-learn exhaustion the hard way (7,412 upstream 429s
   documented in diegosouzapw#3707). markConnectionQuotaExhausted() now persists it to the DB.
3. antigravity429Engine classify429() did not recognise the real Antigravity quota
   message ("Individual quota reached. Contact your administrator to enable overages.")
   — it fell through to the unknown category and never triggered the quota_exhausted
   path. Added "quota reached", "enable overages", "individual quota" to the keywords.
Integrated into release/v3.8.24
…) (diegosouzapw#3764)

The dashboard loaded the Material Symbols icon font only from the Google
Fonts CDN (fonts.googleapis.com). On networks where that CDN is blocked
(e.g. mainland China) the ligature font never loaded, so every icon
rendered as its literal text name and the layout broke. Bundle the font
locally via the material-symbols package so icons render without a
runtime CDN dependency.

Closes diegosouzapw#3695
…erable refresh (diegosouzapw#3679) (diegosouzapw#3766)

The proactive health-check refresh deactivation branch nulled the stored
refresh_token on any unrecoverable error (e.g. invalid_grant). That was
only meant for rotating one-time-use tokens (Codex/OpenAI); for
non-rotating Google-family providers (gemini-cli/antigravity/gemini) it
destroyed the user's only recovery artifact, leaving the connection
permanently showing 'No valid refresh token available'. Gate the null on
isRotatingProvider so non-rotating tokens are preserved.

Closes diegosouzapw#3679
Integrated into release/v3.8.24 — tool-call argument dedup made snapshot-only to prevent silent truncation (fix-in-place + tests).
…redirects as SSRF (diegosouzapw#3288, diegosouzapw#3758) (diegosouzapw#3767)

qwen-web had no specialty validator, so validation fell through to the
generic OpenAI-compatible path which probed a bogus /api/v2/models URL.
That answers with a 307 redirect, the outbound guard blocked it, and the
route surfaced it as provider.validation.ssrf_blocked.

- Add a qwen-web specialty validator that probes the real session
  endpoint (GET /api/v2/user, like Chat2API) with the executor's
  anti-bot headers + cookie-jar replay, bypassing the addModelsSuffix path.
- Harden toValidationErrorResult: a blocked redirect is only a security
  block when its target is a private/internal host; a benign 3xx to a
  public host is no longer mislabeled as SSRF (affected all web-cookie
  providers).

Refs diegosouzapw#3288 diegosouzapw#3758
…gosouzapw#3761) (diegosouzapw#3768)

Importing kimi-k2.7-code via Ollama Cloud's 'import from /models' left it
with the 128000/8192 capability defaults and no vision, because the model
had no spec/registry entry and Ollama Cloud's /v1/models upstream returns
no per-model metadata. Add a global kimi-k2.7-code model spec (parity with
kimi-k2.6: 262K context/output, vision + thinking + tools) and a registry
entry on ollama-cloud so the real capabilities resolve.

Closes diegosouzapw#3761
…3757)

Quality Gates → 100% (Fase 6A + Fase 7 + plano Fase 8). Reconciled file-size + dep allowlist for concurrent v3.8.24 merges. Integrated into release/v3.8.24.
…iegosouzapw#3732) (diegosouzapw#3769)

The Fly.io deployment guide told users to register
<BASE_URL>/api/oauth/<provider>/callback, but OmniRoute's browser OAuth
flow (OAuthModal.startOAuthFlow) uses a single <BASE_URL>/callback handler
— there is no per-provider callback route. The mismatch made GitLab Duo
(and every OAuth provider) reject the flow with 'The redirect URI included
is not valid'. Fix the doc to the real /callback path and add a guard test.

Refs diegosouzapw#3732
…pw#3765)

fix(mcp): resolve server entry from dist/ instead of app/ — matches prepublish output + published files[]. Integrated into release/v3.8.24.
… for concurrent merge diegosouzapw#3768 (diegosouzapw#3770)

Re-baseline providerRegistry.ts file-size for concurrent diegosouzapw#3768 growth. Integrated into release/v3.8.24.
…lve (diegosouzapw#3714) (diegosouzapw#3771)

Pinned Cursor Claude/GPT thinking variants returned an empty turn (502
'Provider returned empty content') because OmniRoute encoded the model
only as RequestedModel (AgentRunRequest field 9) with a bare model_id.
cursor-agent's working wire format also sends a ModelDetails envelope
(field 3) with model_id + display_model_id + display_name; the pinned
thinking variants need it to resolve. Add ModelDetails additively,
keeping RequestedModel (and its -fast parameters) so server-routed ids
(auto/composer) are unaffected.

Closes diegosouzapw#3714
…diegosouzapw#3731) (diegosouzapw#3772)

Two bugs made the Playground model selector unusable for custom
OpenAI/Anthropic-compatible providers:
- when the catalog prefix didn't resolve, the list was filtered by the
  raw connection id (matches nothing) → empty selector ('NONE shown');
- selecting a provider reset the model to '' and nothing picked a
  default → chat failed with 'Set a model in the config pane'.

Extract two pure helpers (resolveModelFilterKey, pickDefaultModel): a
compatible provider without a prefix now falls back to the full catalog
instead of emptying, and the first available model is auto-selected once
the list resolves.

Closes diegosouzapw#3731
…detection (diegosouzapw#3321) (diegosouzapw#3773)

macOS GUI/Electron apps don't inherit the interactive login-shell PATH,
so Homebrew/nvm/volta-installed CLIs (Cline, Codex, OpenCode, Continue,
Hermes, ...) were reported 'not installed' and the Cline runtime couldn't
be spawned. Add a darwin-only, cached, fail-safe login-shell PATH probe
($SHELL -ilc, Hard-Rule-13-safe argv) and merge it into the lookup env in
both cliRuntime.getLookupEnv (provider runtime) and the doctor's
tool-detector. Pure merge/parse helpers are unit-tested with an injected
shell runner.

Closes diegosouzapw#3321
…souzapw#3453)

docs: close proxy/skills/memory/rtk/compression gaps (fabricated refs corrected during review). Integrated into release/v3.8.24.
feat: custom plugin marketplace (GET /api/plugins/marketplace + SSRF-guarded registry). Integrated into release/v3.8.24.
…ELOG to 3.8.24

- Remove internal quality-gate planning docs (PLANO-*, RELATORIO-*),
  openspec/superpowers/spec working notes, and a stale fixes doc that
  should not ship with the release.
- Drop the getting-started glob from the fumadocs source config.
- Add the [3.8.24] section stub to the 41 i18n CHANGELOG mirrors so the
  docs-sync gate is consistent (the EN CHANGELOG already had it).
…ding) (diegosouzapw#3774)

Harden marketplace SSRF guard (IPv6/AAAA + redirect-block + fail-closed resolve). Follow-up to diegosouzapw#3656. Integrated into release/v3.8.24.
… URL substring, esbuild) (diegosouzapw#3778)

Resolve CodeQL (cache-hash FP, URL substring) + Dependabot (esbuild) security alerts. Integrated into release/v3.8.24.
…iegosouzapw#3775)

Pass Claude passthrough thinking blocks through unchanged (fixes the redacted_thinking 400). Live-validated against the Anthropic API. Integrated into release/v3.8.24.
…mperature/top_p) (diegosouzapw#3780)

Claude with extended thinking rejects non-default sampling params (temperature must be 1, top_p >= 0.95 or unset) with HTTP 400. Clients like the VS Code Copilot Ollama BYOK provider send temperature 0.7 + top_p 0.9, breaking every Claude+thinking request across grouped/raw/combo.

enforceThinkingTemperature now also drops top_p (in addition to pinning temperature=1) when thinking is enabled/adaptive, and is called at the final dispatch chokepoint in BaseExecutor (before fingerprint/CCH signing) for claude + claude-code-compatible providers — the single point every routing mode converges on. No-op when thinking is inactive.

Tests: claude-code-parity.test.ts +3 cases (31/31). typecheck:core + eslint clean.

Integrated into release/v3.8.24.
…osouzapw#3777)

Implements end-to-end proxy egress isolation with address-family enforcement and closes four real IP-leak paths surfaced by the proxy subsystem audit.

IPv6-only egress:
- Detect proxy family (IPv6/IPv4 literal, or per-account auto/ipv4/ipv6 directive) and pin the connect family (family:6/4 + autoSelectFamily:false) on both ProxyAgent (proxyTls) and a custom SOCKS connector that threads socket_options.family into SocksClient (fetch-socks can't), so Happy Eyeballs cannot pick IPv4 for an IPv6-only policy.
- De-bracket IPv6-literal proxy hosts (socksOptions.host + proxyHealth tcpCheck) — fixes ENOTFOUND on [::1]-style SOCKS proxies and health checks.
- Fail-closed when an IPv6-only hostname proxy has no AAAA (PROXY_FAMILY_UNAVAILABLE) instead of leaking over v4.
- DB migration 099 adds the proxy family column; family is preserved through the resolveProxyForConnection cascade and proxies/upstreamProxy modules.

Leak fixes:
- L1: web TLS clients (grok/claude/chatgpt/perplexity) now fail-closed on proxy-resolution error instead of silently going direct (was the highest-risk asymmetry).
- L2: safeResolveProxy fail-closed by default (PROXY_FAIL_OPEN opt-out).
- L3: API-key usage/quota fetch routed through the connection proxy context.
- L4: NVIDIA validation proxy bypass (diegosouzapw#3226) documented.

Tests: 46 TDD unit tests + BDD egress-isolation matrix. typecheck:core + eslint clean.
Gemini 'critical' SOCKS finding verified as a false positive (socks accepts proxy.host; see PR thread).

Integrated into release/v3.8.24.
…iegosouzapw#3776)

Adds per-API-key strict-mode controls for the Claude Code default model surface.

- blocked_models deny-list on API keys (deny-list takes precedence over allow-list), so operators can keep a broad dynamic scope like cc/* while excluding expensive families (opus/sonnet/haiku/fable).
- Claude Code default model (cc/*) UI in the API manager: a collapsible families chip group to block individual families through the default model.
- Permission matching expanded with claude-code candidates (cc/, claude/, short aliases, [1m] suffix) so allow and deny match the same candidate set — a blocked family cannot be bypassed via an alias. Setting-dependent claude routing bypasses the permission cache to avoid stale results.
- Model denials on the Anthropic /v1/messages path return a Claude-shaped error (invalid_request_error) via sanitizeErrorMessage instead of the generic 403.

Synced from the v3.8.23 fork point to release/v3.8.24; restored reasoningTokenBufferEnabled (out-of-scope deletion) and re-baselined file-size for the feature growth. Fast Quality Gates + semgrep green; PR unit tests 74/74; typecheck:core + eslint clean.

Integrated into release/v3.8.24.
diegosouzapw and others added 6 commits June 13, 2026 15:23
…contributor credits

- Restructure [3.8.24] into ✨ Features / 🔒 Security / 🐛 Fixed / 📝 Maintenance
- Add bullets for every PR landed since v3.8.23 that was missing:
  marketplace (diegosouzapw#3656), strict-mode CC defaults (diegosouzapw#3776), emergency-fallback flag (diegosouzapw#3752),
  xhigh effort (diegosouzapw#3756), Codex memory WS (diegosouzapw#3749), IPv6 egress (diegosouzapw#3777),
  marketplace SSRF (diegosouzapw#3774), CodeQL/Dependabot (diegosouzapw#3778), anthropic sampling (diegosouzapw#3780),
  thinking passthrough (diegosouzapw#3775), mcp dist entry (diegosouzapw#3765), streamed tool args (diegosouzapw#3762),
  logs light-mode (diegosouzapw#3760), clean-history purge (diegosouzapw#3751), quality-gates (diegosouzapw#3757),
  docs gaps (diegosouzapw#3453), file-size re-baseline (diegosouzapw#3770), E415 publish guard, i18n prune
- Move misplaced diegosouzapw#3775 bullet out of [Unreleased] into [3.8.24]
- Date [3.8.23] header (TBD -> 2026-06-12, the release tag date)
…8.24)

Deep codebase audit corrected stale counts and facts across the root and
docs/ root documentation (verified against source, not from memory):

- MCP tools 43/37/29/69 → 87 (TOTAL_MCP_TOOL_COUNT); MCP scopes ~13 → 30
- DB domain modules 45+/76 → 83; migrations 55/94 → 97 (latest 099)
- Routing strategies 14 → 15 (added missing reset-window)
- A2A skills 5 → 6; provider entries 229 → 232; open-sse services 111 → 115/184
- Node engine ≥20.20.2/22.22.2/24<25 → >=22.0.0 <23 || >=24.0.0 <27
- TypeScript 5.9 → 6.0; README footer v3.8.6 → v3.8.24
- Husky pre-push 'disabled' → active (check:any-budget:t11 + check:tracked-artifacts)
- docs/README.md version 3.8.2 → 3.8.24; fixed duplicate '## architecture/' → '## reference/'
- docs/AGENTROUTER.md wire image: claude-cli 2.1.137 → 2.1.158, +redact-thinking-2026-02-12 beta flag
- docs/SUBMIT_PR.md: version-agnostic release-branch guidance, coverage gate 75/75/75/70 → 60/60/60/60
- AGENTS.md: removed broken open-sse/AGENTS.md link, restamped live-counts to v3.8.24
- README: documented plugin marketplace, Notion/Obsidian, embedded services; fixed broken
  skills/omniroute path + 10→43 skills; added CHANGELOG link; cleaned orphaned <details> tags

check:docs-sync + check:docs-counts green. No new fabricated-docs/symbols drift introduced.
…ES row

The `check:doc-links` table row in docs/architecture/QUALITY_GATES.md (added
this cycle by diegosouzapw#3757) used a literal `[text](path)` example inside inline code.
The link checker strips fenced code blocks but not inline backticks, so it parsed
that example as a real link to a 'path' file that does not exist — turning the
'Docs Sync (Strict)' CI job (npm run check:docs-all) red. Reworded to break the
`](` adjacency; check:doc-links now passes (655 links, 0 broken).
…+ complexity routing (diegosouzapw#3779)

Deep audit of the combo + quota-shared system, delivered as 4 TDD waves.

- Wave 1: repair 5 dead/broken rules — streaming USD recording, pool-usage
  provider resolution, provider-diversity wiring, maxComboDepth threading,
  scoring clamp/NaN-safety (incl. connectionDensity).
- Wave 2: validate every auto-router strategy (cost / latency / sla-aware / lkgp
  / selectWithStrategy + aliases) and the predictive-TTFT decision.
- Wave 3: E2E coverage — 3-hop priority failover, per-target timeout failover,
  real strategy:auto dispatch.
- Wave 4: complexity-aware routing (2026, opt-in) over the existing specificity
  detector, plus revival of the dead tierAffinity / specificityMatch scoring
  factors (require-in-ESM root cause -> static import).

Proxy/credential isolation verified clean (each target uses its own
credentials+proxy via AsyncLocalStorage). file-size reconciled (combo.ts
re-baseline after extracting buildComplexityRoutingHint to complexityRouter.ts;
base.ts release-drift from diegosouzapw#3780). Fast Quality Gates + semgrep green.
- Splits the monolithic usage fetcher into 24 domain-specific files
- Extracts provider-specific usage implementations (Antigravity, Claude, Cursor, GitHub, etc.)
- Uses folder-as-module pattern with index.ts facade for zero backward-compatibility breakages
- Auto-prunes and manages all imports for strict typechecking
@oyi77
oyi77 requested a review from diegosouzapw as a code owner June 13, 2026 20:51

@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 introduces a comprehensive usage fetching module under open-sse/services/usage/ to retrieve and parse quota and usage data from various AI providers. The review feedback highlights critical circular dependency issues, specifically a large dependency web created by the __testing export in utils.ts and a direct circular dependency between antigravity.ts and gemini.ts. Additionally, a potential bug was identified in kimi.ts where multiple rate limits would overwrite the quotas["Ratelimit"] key, which can be resolved by using unique keys.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +152 to +180
export const __testing = {
parseResetTime,
formatGitHubQuotaSnapshot,
inferGitHubPlanName,
getGeminiCliPlanLabel,
getAntigravityPlanLabel,
extractCodeAssistSubscriptionTier,
extractCodeAssistOnboardTierId,
getMiniMaxPlanLabel,
inferMiniMaxPlanLabelFromTotals,
getOpencodeUsage,
getClaudePlanLabel,
createQuotaFromUsage,
getMiniMaxQuotaResetAt,
isMiniMaxTextQuotaModel,
getMiniMaxSessionTotal,
getMiniMaxWeeklyTotal,
createMiniMaxQuotaFromCount,
createMiniMaxQuotaFromPercent,
getMiniMaxRemainingPercent,
getMiniMaxUsage,
getXiaomiMimoUsage,
getMiniMaxAuthErrorMessage,
getMiniMaxErrorSummary,
mapCodeAssistSubscriptionToPlanLabel,
mapCodeAssistTierIdToLabel,
mapSubscriptionTierStringToPlanLabel,
toDisplayLabel,
};

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

The __testing object imports functions from almost every provider file (github.ts, gemini.ts, antigravity.ts, minimax.ts, opencode.ts, claude.ts, and xiaomi.ts), while those provider files simultaneously import core utility functions from utils.ts. This creates a massive circular dependency web across the entire usage/ module, which can lead to runtime issues where imported bindings are undefined during module initialization.

Since these functions are already exported from their respective files, unit tests can (and should) import them directly from their source modules rather than through utils.ts. Removing the __testing export and its associated imports will completely break this circular dependency web.

export {};

Comment on lines +88 to +111
const _usageCacheCleanupTimer = setInterval(
() => {
const now = Date.now();
for (const [key, entry] of _geminiCliSubCache) {
if (now - entry.fetchedAt > GEMINI_CLI_CACHE_TTL_MS) _geminiCliSubCache.delete(key);
}
for (const [key, entry] of _antigravitySubCache) {
if (now - entry.fetchedAt > ANTIGRAVITY_CACHE_TTL_MS) _antigravitySubCache.delete(key);
}
for (const [key, entry] of _antigravityAvailableModelsCache) {
if (now - entry.fetchedAt > ANTIGRAVITY_MODELS_CACHE_TTL_MS)
_antigravityAvailableModelsCache.delete(key);
}
for (const [key, entry] of _antigravityUserQuotaCache) {
if (now - entry.fetchedAt > ANTIGRAVITY_MODELS_CACHE_TTL_MS)
_antigravityUserQuotaCache.delete(key);
}
for (const [key, entry] of _antigravityCreditProbeCache) {
if (now - entry.fetchedAt > ANTIGRAVITY_CREDIT_PROBE_TTL_MS)
_antigravityCreditProbeCache.delete(key);
}
},
5 * 60 * 1000
);

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

There is a direct circular dependency between antigravity.ts and gemini.ts:

  1. antigravity.ts imports _geminiCliSubCache from gemini.ts to clean it up in this global setInterval timer.
  2. gemini.ts imports mapCodeAssistSubscriptionToPlanLabel from antigravity.ts to implement getGeminiCliPlanLabel.

To resolve this:

  • Move the cache cleanup logic for _geminiCliSubCache into gemini.ts (either via its own interval or passive cleanup).
  • Move mapCodeAssistSubscriptionToPlanLabel and its helper functions from antigravity.ts to utils.ts (or a dedicated subscriptionUtils.ts file) so both modules can import it from a common utility file without circular dependencies.

Comment on lines +109 to +129
const limitsArray = Array.isArray(dataObj.limits) ? dataObj.limits : [];
for (let i = 0; i < limitsArray.length; i++) {
const limitItem = toRecord(limitsArray[i]);
const window = toRecord(limitItem.window);
const detail = toRecord(limitItem.detail);

const limit = toNumber(detail.limit || detail.Limit, 0);
const remaining = toNumber(detail.remaining || detail.Remaining, 0);
const resetTime = detail.resetTime || detail.reset_at || detail.resetAt;

if (limit > 0) {
quotas["Ratelimit"] = {
used: limit - remaining,
total: limit,
remaining,
remainingPercentage: limit > 0 ? (remaining / limit) * 100 : 0,
resetAt: parseResetTime(resetTime),
unlimited: 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.

medium

If limitsArray contains multiple rate limits (e.g., RPM and TPM limits), they will all write to quotas["Ratelimit"], meaning only the last limit in the array will be preserved and displayed on the dashboard.

To prevent overwriting, we should use a unique key for each rate limit if there are multiple items.

    const limitsArray = Array.isArray(dataObj.limits) ? dataObj.limits : [];
    for (let i = 0; i < limitsArray.length; i++) {
      const limitItem = toRecord(limitsArray[i]);
      const window = toRecord(limitItem.window);
      const detail = toRecord(limitItem.detail);

      const limit = toNumber(detail.limit || detail.Limit, 0);
      const remaining = toNumber(detail.remaining || detail.Remaining, 0);
      const resetTime = detail.resetTime || detail.reset_at || detail.resetAt;

      if (limit > 0) {
        const key = limitsArray.length > 1 ? `Ratelimit_${i + 1}` : "Ratelimit";
        quotas[key] = {
          used: limit - remaining,
          total: limit,
          remaining,
          remainingPercentage: limit > 0 ? (remaining / limit) * 100 : 0,
          resetAt: parseResetTime(resetTime),
          unlimited: false,
        };
      }
    }

@oyi77

oyi77 commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #3989 — re-cut as a small, independent, non-stacked PR from release/v3.8.27. Closing this stacked version.

@oyi77 oyi77 closed this Jun 16, 2026
@oyi77
oyi77 deleted the pr/3645-clean branch August 7, 2026 21:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants