fix(oauth+errors): OAuth silent refresh, quota STOP patterns, compaction loop cap

Bug fixes:
1. OAuth token refresh (#3149): buildHttpRequestInit() now attempts silent refresh
   via refresh_token before triggering full browser re-auth. Added refresh() method
   to McpOAuthProvider. Includes test isolation fix for discovery mock.

2. Quota error STOP (#3126): Added STOP_MESSAGE_PATTERNS in model-error-classifier
   that take precedence over RETRYABLE_MESSAGE_PATTERNS. Message-only quota errors
   now non-retryable. Runtime-fallback: quota_exceeded with 'retrying in' signal
   still triggers fallback (provider-managed auto-retry). Restored removed patterns.

3. Compaction loop (#3127): MAX_RECOVERY_ATTEMPTS=3 cap + additional suppression
   guard from opencode session in degradation monitor.

Also: refactored extractAutoRetrySignal to auto-retry-signal.ts, new regression
tests for quota classifier and compaction degradation monitor.
This commit is contained in:
YeonGyu-Kim
2026-04-06 17:38:37 +09:00
parent 6c4e0b69a5
commit 61083d499d
15 changed files with 611 additions and 531 deletions
@@ -6,6 +6,7 @@ import { resolveCompactionModel } from "./shared/compaction-model-resolver"
const PREEMPTIVE_COMPACTION_TIMEOUT_MS = 120_000
const POST_COMPACTION_MONITOR_COUNT = 5
const POST_COMPACTION_NO_TEXT_THRESHOLD = 3
const RECOVERY_COMPACTION_SUPPRESSION_MS = 5_000
declare function setTimeout(handler: () => void, timeout?: number): unknown
declare function clearTimeout(timeoutID: unknown): void
@@ -74,6 +75,7 @@ export function createPostCompactionDegradationMonitor(args: {
const postCompactionNoTextStreak = new Map<string, number>()
const postCompactionRecoveryTriggered = new Set<string>()
const postCompactionEpoch = new Map<string, number>()
const suppressRecoveryCompactionUntil = new Map<string, number>()
const postCompactionRecoveryCount = new Map<string, number>()
const MAX_RECOVERY_ATTEMPTS = 3
@@ -87,6 +89,13 @@ export function createPostCompactionDegradationMonitor(args: {
}
const onSessionCompacted = (sessionID: string): void => {
const suppressedUntil = suppressRecoveryCompactionUntil.get(sessionID)
if (suppressedUntil && suppressedUntil > Date.now()) {
suppressRecoveryCompactionUntil.delete(sessionID)
return
}
suppressRecoveryCompactionUntil.delete(sessionID)
const nextEpoch = (postCompactionEpoch.get(sessionID) ?? 0) + 1
postCompactionEpoch.set(sessionID, nextEpoch)
postCompactionRemaining.set(sessionID, POST_COMPACTION_MONITOR_COUNT)
@@ -116,6 +125,7 @@ export function createPostCompactionDegradationMonitor(args: {
postCompactionRecoveryTriggered.add(sessionID)
compactionInProgress.add(sessionID)
const recoveryEpoch = postCompactionEpoch.get(sessionID) ?? 0
suppressRecoveryCompactionUntil.set(sessionID, Date.now() + RECOVERY_COMPACTION_SUPPRESSION_MS)
try {
const { providerID: targetProviderID, modelID: targetModelID } = resolveCompactionModel(
@@ -148,6 +158,7 @@ export function createPostCompactionDegradationMonitor(args: {
log("[preemptive-compaction] Triggered recovery after post-compaction no-text tail", { sessionID })
} catch (error) {
suppressRecoveryCompactionUntil.delete(sessionID)
log("[preemptive-compaction] Failed to recover post-compaction no-text tail", {
sessionID,
error: String(error),