Pause / reprise après quota Claude
Pause / reprise après quota Claude
Dernière mise à jour : 2026-05-10
Le compte Claude utilisé par la VM oracle a un quota Pro/Team (fenêtre 5h glissante). Quand il sature, on veut mettre en pause les jobs en cours et reprendre auto à l'heure de reset, sans perdre le travail déjà fait.
Détection (services/claude-quota.ts)
Le service parse la sortie du CLI Claude pour détecter le message de quota.
Marqueurs supportés
QUOTA_MARKERS = [
'usage limit reached',
"you've hit",
'hit your usage limit',
'hit your session',
'rate limit reached',
// …
]
⚠️ Le scan se fait sur toute la sortie du CLI, même quand Claude a émis du texte assistant — certaines versions du CLI renvoient le message de quota comme une réponse assistant normale au lieu d'exiter en erreur.
Patterns d'extraction de l'heure de reset
Par ordre de priorité :
|<unix_seconds>— timestamp Unix direct (le plus fiable)reset at HH:MM AM/PM(12h)resets at HH:MM AM/PMtry again at HH:MM AM/PMavailable again at HH:MM AM/PMreset at HH:MM(24h, sans AM/PM)- Fallback :
+1hsi rien ne match
→ ClaudeQuotaError avec champ resetAt: Date.
Pattern à respecter partout où Claude est appelé pendant un job long
Émetteurs (claude-ssh.ts, claude-local.ts)
En fin de stream, si rien n'a été généré, appeler throwIfQuota(stderr + streamOutput) avant de yield l'erreur générique. Sinon le quota se transforme en "answer empty" silencieux.
Workers parallèles (pool SSH)
Pour contextual-llm.ts, conflict-detect.ts, etc. :
let quotaError: ClaudeQuotaError | null = null;
while (todoIndex < todo.length && !quotaError) {
const task = todo[todoIndex++];
try {
await processTask(task);
} catch (err) {
if (err instanceof ClaudeQuotaError) {
quotaError = err;
return; // sans compter l'échec comme un fallback
// (le chunk n'est pas en cache, sera retenté à la reprise)
}
throw err; // autres erreurs : propagation normale
}
}
await Promise.all(workers);
saveCache(...); // ⚠️ TOUJOURS flusher AVANT de throw
if (quotaError) throw quotaError;
L'ordre flush THEN throw est critique : si on throw avant le flush, la reprise repart des derniers ~20 chunks au lieu de pile où on s'est arrêté.
Coordinator (ingest/coordinator.ts)
Dans le catch global :
catch (err) {
if (err instanceof ClaudeQuotaError) {
const resetAt = err.resetAt;
const retryAt = new Date(resetAt.getTime() + 2 * 60 * 1000); // grace 2 min
await gamesRepo.update(gameId, {
ingestStatus: 'scheduled',
ingestScheduledAt: retryAt.toISOString(),
});
scheduleIngestStart(gameId, retryAt);
push('quota_pause', { resetAt, retryAt });
// ⚠️ NE PAS passer le jeu en 'error' — ce n'est pas un échec, juste une pause
return;
}
// ...erreur générique
}
Frontend
L'event SSE quota_pause bascule le wizard d'ingestion sur le panneau step === 'scheduled' avec reason: 'quota' (au lieu de 'user' pour la planification volontaire). La copie est dédiée :
"L'oracle se repose. Reprise prévue à HH:MM."
L'utilisateur n'a rien à faire — le scheduler relance automatiquement.
Ajouter un nouveau marker
Si Claude change le wording du message de quota et que la détection rate :
- Vérifier les logs serveur (
/app/data/logs/server.log) pour voir le wording exact - Étendre
QUOTA_MARKERSdansclaude-quota.ts - Si le format de l'heure change, étendre aussi
detectQuotaResetTime(les patterns regex) - Ajouter un test unitaire dans
claude-quota.test.ts(ce service a déjà des tests pour les patterns existants)
Pas dans 'error', jamais
Le quota n'est pas un échec applicatif : c'est une pause normale prévue. Toute logique downstream (UI, monitoring, alerting) doit traiter 'scheduled' + reason='quota' ≠ 'error'.