charness 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +3 -6
- package/DEVELOPMENT.md +15 -0
- package/README.md +111 -38
- package/SETUP.md +121 -16
- package/dist/adapters/adapterEventRouting.d.ts +2 -1
- package/dist/adapters/adapterEventRouting.d.ts.map +1 -1
- package/dist/adapters/adapterEventRouting.js +3 -0
- package/dist/adapters/adapterEventRouting.js.map +1 -1
- package/dist/adapters/adapterNames.d.ts +13 -0
- package/dist/adapters/adapterNames.d.ts.map +1 -1
- package/dist/adapters/adapterNames.js +16 -0
- package/dist/adapters/adapterNames.js.map +1 -1
- package/dist/adapters/backendUnavailableError.d.ts +11 -0
- package/dist/adapters/backendUnavailableError.d.ts.map +1 -0
- package/dist/adapters/backendUnavailableError.js +19 -0
- package/dist/adapters/backendUnavailableError.js.map +1 -0
- package/dist/adapters/claudeCliAdapter.d.ts +29 -6
- package/dist/adapters/claudeCliAdapter.d.ts.map +1 -1
- package/dist/adapters/claudeCliAdapter.js +70 -16
- package/dist/adapters/claudeCliAdapter.js.map +1 -1
- package/dist/adapters/claudeJsonStreamAdapter.d.ts +36 -3
- package/dist/adapters/claudeJsonStreamAdapter.d.ts.map +1 -1
- package/dist/adapters/claudeJsonStreamAdapter.js +93 -30
- package/dist/adapters/claudeJsonStreamAdapter.js.map +1 -1
- package/dist/adapters/createAdapter.d.ts +3 -14
- package/dist/adapters/createAdapter.d.ts.map +1 -1
- package/dist/adapters/createAdapter.js +9 -19
- package/dist/adapters/createAdapter.js.map +1 -1
- package/dist/adapters/openCodeAdapter.d.ts +140 -12
- package/dist/adapters/openCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/openCodeAdapter.js +385 -119
- package/dist/adapters/openCodeAdapter.js.map +1 -1
- package/dist/apiErrorRetry.d.ts +70 -6
- package/dist/apiErrorRetry.d.ts.map +1 -1
- package/dist/apiErrorRetry.js +312 -34
- package/dist/apiErrorRetry.js.map +1 -1
- package/dist/apiRetryKick.d.ts +26 -1
- package/dist/apiRetryKick.d.ts.map +1 -1
- package/dist/apiRetryKick.js +41 -2
- package/dist/apiRetryKick.js.map +1 -1
- package/dist/bot.d.ts +2 -1
- package/dist/bot.d.ts.map +1 -1
- package/dist/bot.js +736 -184
- package/dist/bot.js.map +1 -1
- package/dist/cli/claudeProfiles.d.ts +1 -1
- package/dist/cli/claudeProfiles.js +1 -1
- package/dist/cli/hot.d.ts +7 -1
- package/dist/cli/hot.d.ts.map +1 -1
- package/dist/cli/hot.js +29 -3
- package/dist/cli/hot.js.map +1 -1
- package/dist/configDirFile.d.ts +73 -0
- package/dist/configDirFile.d.ts.map +1 -0
- package/dist/configDirFile.js +216 -0
- package/dist/configDirFile.js.map +1 -0
- package/dist/connectors/telegram/commands/autoContinueLimits.d.ts +2 -2
- package/dist/connectors/telegram/commands/autoContinueLimits.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/autoContinueLimits.js +4 -4
- package/dist/connectors/telegram/commands/autoContinueLimits.js.map +1 -1
- package/dist/connectors/telegram/commands/backendSwitch.d.ts +25 -0
- package/dist/connectors/telegram/commands/backendSwitch.d.ts.map +1 -0
- package/dist/connectors/telegram/commands/backendSwitch.js +104 -0
- package/dist/connectors/telegram/commands/backendSwitch.js.map +1 -0
- package/dist/connectors/telegram/commands/callbackMessage.d.ts +12 -0
- package/dist/connectors/telegram/commands/callbackMessage.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/callbackMessage.js +24 -0
- package/dist/connectors/telegram/commands/callbackMessage.js.map +1 -1
- package/dist/connectors/telegram/commands/callbackToast.d.ts +36 -0
- package/dist/connectors/telegram/commands/callbackToast.d.ts.map +1 -0
- package/dist/connectors/telegram/commands/callbackToast.js +83 -0
- package/dist/connectors/telegram/commands/callbackToast.js.map +1 -0
- package/dist/connectors/telegram/commands/claudeAccounts.d.ts +18 -0
- package/dist/connectors/telegram/commands/claudeAccounts.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/claudeAccounts.js +220 -66
- package/dist/connectors/telegram/commands/claudeAccounts.js.map +1 -1
- package/dist/connectors/telegram/commands/displayModes.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/displayModes.js +4 -2
- package/dist/connectors/telegram/commands/displayModes.js.map +1 -1
- package/dist/connectors/telegram/commands/fallbackChain.d.ts +5 -8
- package/dist/connectors/telegram/commands/fallbackChain.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/fallbackChain.js +183 -193
- package/dist/connectors/telegram/commands/fallbackChain.js.map +1 -1
- package/dist/connectors/telegram/commands/limitsBoard.d.ts +40 -0
- package/dist/connectors/telegram/commands/limitsBoard.d.ts.map +1 -0
- package/dist/connectors/telegram/commands/limitsBoard.js +281 -0
- package/dist/connectors/telegram/commands/limitsBoard.js.map +1 -0
- package/dist/connectors/telegram/commands/modelPage.d.ts +130 -0
- package/dist/connectors/telegram/commands/modelPage.d.ts.map +1 -0
- package/dist/connectors/telegram/commands/modelPage.js +120 -0
- package/dist/connectors/telegram/commands/modelPage.js.map +1 -0
- package/dist/connectors/telegram/commands/modelProviders.d.ts +9 -1
- package/dist/connectors/telegram/commands/modelProviders.d.ts.map +1 -1
- package/dist/connectors/telegram/commands/modelProviders.js +203 -111
- package/dist/connectors/telegram/commands/modelProviders.js.map +1 -1
- package/dist/connectors/telegram/commands/stt.d.ts +32 -0
- package/dist/connectors/telegram/commands/stt.d.ts.map +1 -0
- package/dist/connectors/telegram/commands/stt.js +460 -0
- package/dist/connectors/telegram/commands/stt.js.map +1 -0
- package/dist/effortLevels.d.ts +57 -7
- package/dist/effortLevels.d.ts.map +1 -1
- package/dist/effortLevels.js +65 -9
- package/dist/effortLevels.js.map +1 -1
- package/dist/i18n/de.d.ts.map +1 -1
- package/dist/i18n/de.js +160 -17
- package/dist/i18n/de.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +160 -17
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +160 -17
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/fr.d.ts.map +1 -1
- package/dist/i18n/fr.js +160 -17
- package/dist/i18n/fr.js.map +1 -1
- package/dist/i18n/hi.d.ts.map +1 -1
- package/dist/i18n/hi.js +160 -17
- package/dist/i18n/hi.js.map +1 -1
- package/dist/i18n/ja.d.ts.map +1 -1
- package/dist/i18n/ja.js +160 -17
- package/dist/i18n/ja.js.map +1 -1
- package/dist/i18n/ka.d.ts.map +1 -1
- package/dist/i18n/ka.js +160 -17
- package/dist/i18n/ka.js.map +1 -1
- package/dist/i18n/pt.d.ts.map +1 -1
- package/dist/i18n/pt.js +160 -17
- package/dist/i18n/pt.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +160 -17
- package/dist/i18n/ru.js.map +1 -1
- package/dist/i18n/uk.d.ts.map +1 -1
- package/dist/i18n/uk.js +160 -17
- package/dist/i18n/uk.js.map +1 -1
- package/dist/i18n/uz.d.ts.map +1 -1
- package/dist/i18n/uz.js +160 -17
- package/dist/i18n/uz.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +160 -17
- package/dist/i18n/zh.js.map +1 -1
- package/dist/installManager.d.ts +20 -4
- package/dist/installManager.d.ts.map +1 -1
- package/dist/installManager.js +82 -14
- package/dist/installManager.js.map +1 -1
- package/dist/instanceSettings.d.ts +52 -0
- package/dist/instanceSettings.d.ts.map +1 -0
- package/dist/instanceSettings.js +136 -0
- package/dist/instanceSettings.js.map +1 -0
- package/dist/pinnedStatus.d.ts +7 -1
- package/dist/pinnedStatus.d.ts.map +1 -1
- package/dist/pinnedStatus.js +1 -1
- package/dist/pinnedStatus.js.map +1 -1
- package/dist/requests/requestLedger.d.ts +8 -0
- package/dist/requests/requestLedger.d.ts.map +1 -1
- package/dist/requests/requestLedger.js +19 -1
- package/dist/requests/requestLedger.js.map +1 -1
- package/dist/scheduler/mcpSurface.js +4 -4
- package/dist/scheduler/mcpSurface.js.map +1 -1
- package/dist/shutdown.d.ts +14 -0
- package/dist/shutdown.d.ts.map +1 -1
- package/dist/shutdown.js +16 -0
- package/dist/shutdown.js.map +1 -1
- package/dist/state.d.ts +54 -2
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +62 -1
- package/dist/state.js.map +1 -1
- package/dist/stt/sttAutoInstall.d.ts +39 -0
- package/dist/stt/sttAutoInstall.d.ts.map +1 -0
- package/dist/stt/sttAutoInstall.js +158 -0
- package/dist/stt/sttAutoInstall.js.map +1 -0
- package/dist/stt/sttChildEnv.d.ts +11 -0
- package/dist/stt/sttChildEnv.d.ts.map +1 -0
- package/dist/stt/sttChildEnv.js +30 -0
- package/dist/stt/sttChildEnv.js.map +1 -0
- package/dist/stt/sttConfigEdit.d.ts +71 -0
- package/dist/stt/sttConfigEdit.d.ts.map +1 -0
- package/dist/stt/sttConfigEdit.js +144 -0
- package/dist/stt/sttConfigEdit.js.map +1 -0
- package/dist/stt/sttInstall.d.ts +147 -0
- package/dist/stt/sttInstall.d.ts.map +1 -0
- package/dist/stt/sttInstall.js +491 -0
- package/dist/stt/sttInstall.js.map +1 -0
- package/dist/stt/sttInstallEntry.d.ts +2 -0
- package/dist/stt/sttInstallEntry.d.ts.map +1 -0
- package/dist/stt/sttInstallEntry.js +31 -0
- package/dist/stt/sttInstallEntry.js.map +1 -0
- package/dist/stt/sttInstallReporter.d.ts +45 -0
- package/dist/stt/sttInstallReporter.d.ts.map +1 -0
- package/dist/stt/sttInstallReporter.js +57 -0
- package/dist/stt/sttInstallReporter.js.map +1 -0
- package/dist/stt/sttRuntime.d.ts +35 -0
- package/dist/stt/sttRuntime.d.ts.map +1 -0
- package/dist/stt/sttRuntime.js +73 -0
- package/dist/stt/sttRuntime.js.map +1 -0
- package/dist/stt/sttScreenPlan.d.ts +193 -0
- package/dist/stt/sttScreenPlan.d.ts.map +1 -0
- package/dist/stt/sttScreenPlan.js +541 -0
- package/dist/stt/sttScreenPlan.js.map +1 -0
- package/dist/stt/sttSettings.d.ts +201 -0
- package/dist/stt/sttSettings.d.ts.map +1 -0
- package/dist/stt/sttSettings.js +387 -0
- package/dist/stt/sttSettings.js.map +1 -0
- package/dist/stt/sttSidecarOwner.d.ts +115 -0
- package/dist/stt/sttSidecarOwner.d.ts.map +1 -0
- package/dist/stt/sttSidecarOwner.js +475 -0
- package/dist/stt/sttSidecarOwner.js.map +1 -0
- package/dist/stt/transcribeChain.d.ts +36 -0
- package/dist/stt/transcribeChain.d.ts.map +1 -0
- package/dist/stt/transcribeChain.js +42 -0
- package/dist/stt/transcribeChain.js.map +1 -0
- package/dist/types.d.ts +77 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/adapterSwitchSessionIds.d.ts +32 -0
- package/dist/utils/adapterSwitchSessionIds.d.ts.map +1 -0
- package/dist/utils/adapterSwitchSessionIds.js +44 -0
- package/dist/utils/adapterSwitchSessionIds.js.map +1 -0
- package/dist/utils/backendSwitchPlan.d.ts +63 -0
- package/dist/utils/backendSwitchPlan.d.ts.map +1 -0
- package/dist/utils/backendSwitchPlan.js +73 -0
- package/dist/utils/backendSwitchPlan.js.map +1 -0
- package/dist/utils/claudeAccountPickerPlan.d.ts +12 -1
- package/dist/utils/claudeAccountPickerPlan.d.ts.map +1 -1
- package/dist/utils/claudeAccountPickerPlan.js +20 -1
- package/dist/utils/claudeAccountPickerPlan.js.map +1 -1
- package/dist/utils/claudeTranscriptContent.d.ts +24 -0
- package/dist/utils/claudeTranscriptContent.d.ts.map +1 -0
- package/dist/utils/claudeTranscriptContent.js +107 -0
- package/dist/utils/claudeTranscriptContent.js.map +1 -0
- package/dist/utils/claudeUsage.d.ts +47 -8
- package/dist/utils/claudeUsage.d.ts.map +1 -1
- package/dist/utils/claudeUsage.js +76 -14
- package/dist/utils/claudeUsage.js.map +1 -1
- package/dist/utils/claudeUsageRefresher.d.ts +8 -1
- package/dist/utils/claudeUsageRefresher.d.ts.map +1 -1
- package/dist/utils/claudeUsageRefresher.js +9 -4
- package/dist/utils/claudeUsageRefresher.js.map +1 -1
- package/dist/utils/effortPickerPlan.d.ts +54 -0
- package/dist/utils/effortPickerPlan.d.ts.map +1 -0
- package/dist/utils/effortPickerPlan.js +56 -0
- package/dist/utils/effortPickerPlan.js.map +1 -0
- package/dist/utils/ensureSessionPlan.d.ts +20 -4
- package/dist/utils/ensureSessionPlan.d.ts.map +1 -1
- package/dist/utils/ensureSessionPlan.js +17 -4
- package/dist/utils/ensureSessionPlan.js.map +1 -1
- package/dist/utils/fallbackBridge.d.ts +6 -2
- package/dist/utils/fallbackBridge.d.ts.map +1 -1
- package/dist/utils/fallbackBridge.js +10 -3
- package/dist/utils/fallbackBridge.js.map +1 -1
- package/dist/utils/fallbackChain.d.ts +36 -0
- package/dist/utils/fallbackChain.d.ts.map +1 -1
- package/dist/utils/fallbackChain.js +65 -1
- package/dist/utils/fallbackChain.js.map +1 -1
- package/dist/utils/fallbackSwitchNotice.d.ts +41 -0
- package/dist/utils/fallbackSwitchNotice.d.ts.map +1 -0
- package/dist/utils/fallbackSwitchNotice.js +75 -0
- package/dist/utils/fallbackSwitchNotice.js.map +1 -0
- package/dist/utils/fileSendService.d.ts +33 -10
- package/dist/utils/fileSendService.d.ts.map +1 -1
- package/dist/utils/fileSendService.js +78 -27
- package/dist/utils/fileSendService.js.map +1 -1
- package/dist/utils/jsonStreamHost.d.ts +39 -0
- package/dist/utils/jsonStreamHost.d.ts.map +1 -1
- package/dist/utils/jsonStreamHost.js +60 -1
- package/dist/utils/jsonStreamHost.js.map +1 -1
- package/dist/utils/limitEpisodeRecovery.d.ts +16 -2
- package/dist/utils/limitEpisodeRecovery.d.ts.map +1 -1
- package/dist/utils/limitEpisodeRecovery.js +44 -3
- package/dist/utils/limitEpisodeRecovery.js.map +1 -1
- package/dist/utils/limitSettings.d.ts +25 -0
- package/dist/utils/limitSettings.d.ts.map +1 -0
- package/dist/utils/limitSettings.js +31 -0
- package/dist/utils/limitSettings.js.map +1 -0
- package/dist/utils/limitsBoardPlan.d.ts +52 -0
- package/dist/utils/limitsBoardPlan.d.ts.map +1 -0
- package/dist/utils/limitsBoardPlan.js +93 -0
- package/dist/utils/limitsBoardPlan.js.map +1 -0
- package/dist/utils/openCodeUsage.d.ts +93 -0
- package/dist/utils/openCodeUsage.d.ts.map +1 -0
- package/dist/utils/openCodeUsage.js +248 -0
- package/dist/utils/openCodeUsage.js.map +1 -0
- package/dist/utils/operatorSwitchPlan.d.ts +61 -0
- package/dist/utils/operatorSwitchPlan.d.ts.map +1 -0
- package/dist/utils/operatorSwitchPlan.js +32 -0
- package/dist/utils/operatorSwitchPlan.js.map +1 -0
- package/dist/utils/promptDelivery.d.ts +40 -0
- package/dist/utils/promptDelivery.d.ts.map +1 -1
- package/dist/utils/promptDelivery.js +31 -0
- package/dist/utils/promptDelivery.js.map +1 -1
- package/dist/utils/recvPreviewRedaction.d.ts +14 -4
- package/dist/utils/recvPreviewRedaction.d.ts.map +1 -1
- package/dist/utils/recvPreviewRedaction.js +19 -7
- package/dist/utils/recvPreviewRedaction.js.map +1 -1
- package/dist/utils/startupReadiness.d.ts +3 -3
- package/dist/utils/startupReadiness.d.ts.map +1 -1
- package/dist/utils/startupReadiness.js +2 -2
- package/dist/utils/startupReadiness.js.map +1 -1
- package/dist/utils/threadStatusReport.d.ts +3 -1
- package/dist/utils/threadStatusReport.d.ts.map +1 -1
- package/dist/utils/threadStatusReport.js +4 -3
- package/dist/utils/threadStatusReport.js.map +1 -1
- package/dist/utils/timezone.d.ts +23 -0
- package/dist/utils/timezone.d.ts.map +1 -1
- package/dist/utils/timezone.js +38 -0
- package/dist/utils/timezone.js.map +1 -1
- package/dist/utils/transcribeAudio.d.ts +25 -12
- package/dist/utils/transcribeAudio.d.ts.map +1 -1
- package/dist/utils/transcribeAudio.js +39 -21
- package/dist/utils/transcribeAudio.js.map +1 -1
- package/dist/utils/usageRefreshBudget.d.ts +9 -0
- package/dist/utils/usageRefreshBudget.d.ts.map +1 -0
- package/dist/utils/usageRefreshBudget.js +27 -0
- package/dist/utils/usageRefreshBudget.js.map +1 -0
- package/examples/.env.example +7 -3
- package/examples/docker-compose.yml +9 -7
- package/package.json +2 -1
- package/stt/gigaam_sidecar.py +800 -0
package/dist/apiErrorRetry.d.ts
CHANGED
|
@@ -22,6 +22,21 @@ import type { AgentApiErrorClass } from './types';
|
|
|
22
22
|
export declare const transientBackoffMinutes: readonly [5, 10, 20];
|
|
23
23
|
/** Max transient attempts before giving up (= `transientBackoffMinutes.length`). */
|
|
24
24
|
export declare const transientMaxAttempts: 3;
|
|
25
|
+
/**
|
|
26
|
+
* A limit whose stated reset is at most this far ahead is a throttle: the first transient retry already outwaits it.
|
|
27
|
+
* Google's subscription quotas say "Your quota will reset after 2s" for a momentary cap and "after 146h52m11s" for a
|
|
28
|
+
* spent plan — the same words, told apart only by the duration.
|
|
29
|
+
*/
|
|
30
|
+
export declare const shortStatedResetMaxMs: number;
|
|
31
|
+
/** @description Whether a stated reset (a parsed text reset or a Retry-After field) is within {@link shortStatedResetMaxMs} ahead. */
|
|
32
|
+
export declare function checkIsShortStatedReset(resetAt: number, now: number): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* The retry of a request the provider refused because the conversation ended on the model's turn
|
|
35
|
+
* ({@link conversationTailRe}): the "continue" nudge is a user turn, which is all the provider asks for — so it goes at
|
|
36
|
+
* once (a moment, to let the failed turn settle), and only once: the same refusal right after it ends with the give-up.
|
|
37
|
+
*/
|
|
38
|
+
export declare const conversationTailRetryDelayMs = 2000;
|
|
39
|
+
export declare const conversationTailMaxAttempts = 1;
|
|
25
40
|
/**
|
|
26
41
|
* Fixed delay for a usage-limit error whose text exposed no reset time (the
|
|
27
42
|
* common case for Claude's "blocked" message): 60 minutes, re-armed on each
|
|
@@ -61,6 +76,13 @@ export declare const authErrorPhrasesRe: RegExp;
|
|
|
61
76
|
export declare const usageLimitPhraseSource: string;
|
|
62
77
|
/** {@link usageLimitPhraseSource} compiled for a whole-text `.test()`. */
|
|
63
78
|
export declare const usageLimitPhrasesRe: RegExp;
|
|
79
|
+
/**
|
|
80
|
+
* @description Whether a text is a usage WARNING (see {@link usageLimitWarningRe})
|
|
81
|
+
* — the account is still allowed, so it is never a usage-limit error. Shared with
|
|
82
|
+
* the Claude scrape detector (`getClaudeAgentErrorLine`), which skips such a row so
|
|
83
|
+
* a real block further down the same pane delta is still found.
|
|
84
|
+
*/
|
|
85
|
+
export declare function checkIsUsageLimitWarningText(text: string): boolean;
|
|
64
86
|
/**
|
|
65
87
|
* @description Whether an error text is a usage / session / window limit.
|
|
66
88
|
*
|
|
@@ -84,8 +106,18 @@ export declare function checkIsUsageLimitText(text: string): boolean;
|
|
|
84
106
|
* 1. auth / logged out (login, bad credentials) → `{ kind: 'auth' }`. Must win:
|
|
85
107
|
* a wait never fixes these (they need a re-login or server restart), so they
|
|
86
108
|
* are SURFACED (a pinned notice), never auto-retried. Used to return `null`.
|
|
109
|
+
* 1a. the refusal of a history that ends on the model's turn ({@link conversationTailRe}) → `conversationTail`.
|
|
110
|
+
* 1b. a usage WARNING ({@link checkIsUsageLimitWarningText}) → `null`: the
|
|
111
|
+
* request went through; it names a window and a reset like the block does.
|
|
112
|
+
* 1b'. a stated reset ({@link parseResetAt}) within {@link shortStatedResetMaxMs} → `transient`;
|
|
113
|
+
* a longer one keeps 1c from calling the limit short. A short one a RETRY verb states ("retry in 40s") that reaches
|
|
114
|
+
* the usage-limit branches below (a long window named beside it) is a retry hint: their `resetAt` drops it.
|
|
115
|
+
* 1c. a SHORT-window throttle ({@link shortWindowThrottleRe}, reported as hit —
|
|
116
|
+
* {@link limitHitRe}) that names no long
|
|
117
|
+
* window ({@link longWindowRe}) → `transient`: per-minute quotas, concurrency,
|
|
118
|
+
* "retry in 40s" — ahead of the quota codes, which they share with daily quotas.
|
|
87
119
|
* 2. an exhausted-quota provider CODE ({@link exhaustedQuotaCodesRe}) or the observed
|
|
88
|
-
* subscription wording ({@link subscriptionLimitMessageRe}) → `usageLimit`.
|
|
120
|
+
* subscription wording ({@link subscriptionLimitMessageRe}, flagged `isUnconfirmed`) → `usageLimit`.
|
|
89
121
|
* Ahead of transient because these ride a 429 (OpenAI `insufficient_quota`,
|
|
90
122
|
* Google `RESOURCE_EXHAUSTED`), which the next branch would read as a hiccup.
|
|
91
123
|
* 3. transient (rate-limited, overloaded, 429/503/529, or a bare "rate limit" without an explicit
|
|
@@ -101,18 +133,48 @@ export declare function checkIsUsageLimitText(text: string): boolean;
|
|
|
101
133
|
* reset time for the usage class; the transient/auth branches ignore it.
|
|
102
134
|
*/
|
|
103
135
|
export declare function classifyAgentApiError(text: string, now: number): AgentApiErrorClass | null;
|
|
136
|
+
/**
|
|
137
|
+
* The short retry an UNCONFIRMED usage limit ({@link AgentApiErrorClass.isUnconfirmed}) gets first: long enough not to
|
|
138
|
+
* hammer an account that is really limited, short enough that a refused single request costs the topic two minutes, not
|
|
139
|
+
* the hours to the window's reset.
|
|
140
|
+
*/
|
|
141
|
+
export declare const unconfirmedLimitProbeDelayMs: number;
|
|
142
|
+
/**
|
|
143
|
+
* @description What an UNCONFIRMED usage limit does: `probe` — a short retry ({@link unconfirmedLimitProbeDelayMs}),
|
|
144
|
+
* no limit notice; `waitForReset` — the usual usage-limit wait, once the limit is confirmed: it repeated right after
|
|
145
|
+
* the probe (`isRepeatAfterProbe`), or the account's usage reading shows a window at 100 % (`isUsageExhausted`).
|
|
146
|
+
*/
|
|
147
|
+
export declare function getUnconfirmedLimitAction(input: {
|
|
148
|
+
isRepeatAfterProbe: boolean;
|
|
149
|
+
isUsageExhausted: boolean;
|
|
150
|
+
}): 'probe' | 'waitForReset';
|
|
151
|
+
/**
|
|
152
|
+
* @description Whether a usage-limit error names a reset that is already behind
|
|
153
|
+
* `now`: the window is over. The boot recovery of a parked error (read against the
|
|
154
|
+
* time it was written) uses it to resume without announcing a limit; a live error
|
|
155
|
+
* is announced whatever its reset says.
|
|
156
|
+
*/
|
|
157
|
+
export declare function checkIsUsageLimitWindowOver(cls: AgentApiErrorClass, now: number): boolean;
|
|
104
158
|
/**
|
|
105
159
|
* @description Best-effort parse of a "reset time" out of a usage-limit message.
|
|
106
160
|
* Never throws: any unparseable / absent time yields `undefined`, which the
|
|
107
|
-
* caller treats as "use the fixed delay". Handles
|
|
108
|
-
* -
|
|
161
|
+
* caller treats as "use the fixed delay". Handles five shapes, in order:
|
|
162
|
+
* - machine form: an epoch (`"resets_at":…` in a relayed ChatGPT body, Claude's
|
|
163
|
+
* `limit reached|<epoch>`) or that body's `"resets_in_seconds":…` — exact, so
|
|
164
|
+
* first ({@link parseStructuredResetAt}).
|
|
165
|
+
* - relative: "resets in 2h" / "in 45m" / "in 2 hours 30 minutes" → `now` + the
|
|
166
|
+
* sum of the parts.
|
|
167
|
+
* - dated: "resets Oct 25, 3pm (Europe/Moscow)" — a weekly window, more than a
|
|
168
|
+
* day ahead → that date and clock, in the stated zone (or the local one).
|
|
109
169
|
* - absolute clock: "resets at 3pm" / "at 15:00" / "resets 10:50pm (UTC)" → the
|
|
110
170
|
* NEXT occurrence of that clock time at or after `now`. An EXPLICIT zone
|
|
111
|
-
* suffix
|
|
112
|
-
*
|
|
171
|
+
* suffix — an offset (`(UTC)`, `+05:00`) or a named zone (`(Europe/Moscow)`,
|
|
172
|
+
* the way Claude Code writes it) — is load-bearing: the clock is then resolved
|
|
173
|
+
* IN THAT ZONE, not in the instance/host zone. Without it a `10:50pm (UTC)` reset on a `+04:00` box
|
|
113
174
|
* resolved four hours EARLY — the retry fires while the limit still holds,
|
|
114
175
|
* re-errors, and burns one of the six attempts for nothing.
|
|
115
|
-
* - ISO timestamp anywhere in the text →
|
|
176
|
+
* - ISO timestamp anywhere in the text → that instant; a zone-less one only with
|
|
177
|
+
* a zone right after it (see {@link resolveIsoResetTime}).
|
|
116
178
|
*
|
|
117
179
|
* A clock shape that fails to resolve (out-of-range hour) falls THROUGH to the
|
|
118
180
|
* ISO branch rather than giving up, so a message carrying both still parses.
|
|
@@ -142,6 +204,8 @@ export type RetryPlan = {
|
|
|
142
204
|
* time never overflows the timer.
|
|
143
205
|
* - transient: {@link transientBackoffMinutes}[attempt-1]; giveUp once attempt
|
|
144
206
|
* exceeds {@link transientMaxAttempts}.
|
|
207
|
+
* - conversationTail: {@link conversationTailRetryDelayMs}; giveUp once attempt exceeds
|
|
208
|
+
* {@link conversationTailMaxAttempts}.
|
|
145
209
|
* - usageLimit: `resetAt` present → `resetAt - now + resetBufferMs`; else
|
|
146
210
|
* {@link usageLimitDefaultMs}; giveUp once attempt exceeds
|
|
147
211
|
* {@link usageLimitMaxAttempts}.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apiErrorRetry.d.ts","sourceRoot":"","sources":["../src/apiErrorRetry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"apiErrorRetry.d.ts","sourceRoot":"","sources":["../src/apiErrorRetry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAUlD;;;GAGG;AACH,eAAO,MAAM,uBAAuB,sBAAuB,CAAC;AAC5D,oFAAoF;AACpF,eAAO,MAAM,oBAAoB,GAAiC,CAAC;AACnE;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,QAA2C,CAAC;AAE9E,sIAAsI;AACtI,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAE7E;AAED;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,OAAQ,CAAC;AAClD,eAAO,MAAM,2BAA2B,IAAI,CAAC;AAE7C;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,QAAmB,CAAC;AACpD,8EAA8E;AAC9E,eAAO,MAAM,qBAAqB,IAAI,CAAC;AACvC;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,QAAkB,CAAC;AACtD;;;GAGG;AACH,eAAO,MAAM,aAAa,QAAS,CAAC;AAyDpC;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,QAAyE,CAAC;AA4EzG;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,QAGxB,CAAC;AAEZ,0EAA0E;AAC1E,eAAO,MAAM,mBAAmB,QAA0C,CAAC;AAkB3E;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAElE;AAsBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAI3D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAsC1F;AAgBD;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,QAAkB,CAAC;AAE5D;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE;IAAE,kBAAkB,EAAE,OAAO,CAAC;IAAC,gBAAgB,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,GAAG,cAAc,CAErI;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAEzF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CA+B1E;AAkJD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC;IACjC,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,SAAS,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,IAAI,CAAA;CAAE,CAAC;AAE/D;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS,CAmB3D;AAOD;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,2DAA2D;IAC3D,OAAO,EAAE,MAAM,CAAC;IAChB,qFAAqF;IACrF,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,+DAA+D;IAC/D,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,QAAQ,CAAA;CAAE,GACpB;IAAE,MAAM,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACnE;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3C,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC;IACjC,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wBAAwB;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,qEAAqE;IACrE,IAAI,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACjC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,qBAAqB,GAAG,WAAW,CAW1E"}
|
package/dist/apiErrorRetry.js
CHANGED
|
@@ -16,15 +16,21 @@
|
|
|
16
16
|
* through `session.error`.
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.usageLimitPhrasesRe = exports.usageLimitPhraseSource = exports.authErrorPhrasesRe = exports.resetBufferMs = exports.retryRecurrenceGraceMs = exports.usageLimitMaxAttempts = exports.usageLimitDefaultMs = exports.transientMaxAttempts = exports.transientBackoffMinutes = void 0;
|
|
19
|
+
exports.unconfirmedLimitProbeDelayMs = exports.usageLimitPhrasesRe = exports.usageLimitPhraseSource = exports.authErrorPhrasesRe = exports.resetBufferMs = exports.retryRecurrenceGraceMs = exports.usageLimitMaxAttempts = exports.usageLimitDefaultMs = exports.conversationTailMaxAttempts = exports.conversationTailRetryDelayMs = exports.shortStatedResetMaxMs = exports.transientMaxAttempts = exports.transientBackoffMinutes = void 0;
|
|
20
|
+
exports.checkIsShortStatedReset = checkIsShortStatedReset;
|
|
21
|
+
exports.checkIsUsageLimitWarningText = checkIsUsageLimitWarningText;
|
|
20
22
|
exports.checkIsUsageLimitText = checkIsUsageLimitText;
|
|
21
23
|
exports.classifyAgentApiError = classifyAgentApiError;
|
|
24
|
+
exports.getUnconfirmedLimitAction = getUnconfirmedLimitAction;
|
|
25
|
+
exports.checkIsUsageLimitWindowOver = checkIsUsageLimitWindowOver;
|
|
22
26
|
exports.parseResetAt = parseResetAt;
|
|
23
27
|
exports.getRetryPlan = getRetryPlan;
|
|
24
28
|
exports.decideRetryAction = decideRetryAction;
|
|
25
29
|
const engine_1 = require("./scheduler/engine");
|
|
30
|
+
const timezone_1 = require("./utils/timezone");
|
|
31
|
+
const msPerSecond = 1000;
|
|
26
32
|
/** One minute in milliseconds — the unit the backoff schedules are expressed in. */
|
|
27
|
-
const msPerMinute =
|
|
33
|
+
const msPerMinute = 60 * msPerSecond;
|
|
28
34
|
/** One hour in milliseconds. */
|
|
29
35
|
const msPerHour = 60 * msPerMinute;
|
|
30
36
|
/**
|
|
@@ -34,6 +40,23 @@ const msPerHour = 60 * msPerMinute;
|
|
|
34
40
|
exports.transientBackoffMinutes = [5, 10, 20];
|
|
35
41
|
/** Max transient attempts before giving up (= `transientBackoffMinutes.length`). */
|
|
36
42
|
exports.transientMaxAttempts = exports.transientBackoffMinutes.length;
|
|
43
|
+
/**
|
|
44
|
+
* A limit whose stated reset is at most this far ahead is a throttle: the first transient retry already outwaits it.
|
|
45
|
+
* Google's subscription quotas say "Your quota will reset after 2s" for a momentary cap and "after 146h52m11s" for a
|
|
46
|
+
* spent plan — the same words, told apart only by the duration.
|
|
47
|
+
*/
|
|
48
|
+
exports.shortStatedResetMaxMs = exports.transientBackoffMinutes[0] * msPerMinute;
|
|
49
|
+
/** @description Whether a stated reset (a parsed text reset or a Retry-After field) is within {@link shortStatedResetMaxMs} ahead. */
|
|
50
|
+
function checkIsShortStatedReset(resetAt, now) {
|
|
51
|
+
return resetAt >= now && resetAt - now <= exports.shortStatedResetMaxMs;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The retry of a request the provider refused because the conversation ended on the model's turn
|
|
55
|
+
* ({@link conversationTailRe}): the "continue" nudge is a user turn, which is all the provider asks for — so it goes at
|
|
56
|
+
* once (a moment, to let the failed turn settle), and only once: the same refusal right after it ends with the give-up.
|
|
57
|
+
*/
|
|
58
|
+
exports.conversationTailRetryDelayMs = 2_000;
|
|
59
|
+
exports.conversationTailMaxAttempts = 1;
|
|
37
60
|
/**
|
|
38
61
|
* Fixed delay for a usage-limit error whose text exposed no reset time (the
|
|
39
62
|
* common case for Claude's "blocked" message): 60 minutes, re-armed on each
|
|
@@ -55,8 +78,23 @@ exports.retryRecurrenceGraceMs = 2 * msPerMinute;
|
|
|
55
78
|
* actually rolls over, never a hair before it (which would re-error instantly).
|
|
56
79
|
*/
|
|
57
80
|
exports.resetBufferMs = 60_000;
|
|
58
|
-
/**
|
|
59
|
-
|
|
81
|
+
/**
|
|
82
|
+
* A reset relayed in MACHINE form — an exact epoch in seconds, read before any prose: `"resets_at":1781085600` in the
|
|
83
|
+
* ChatGPT subscription's `usage_limit_reached` 429 body (codex-rs `UsageErrorBody`), which OpenCode relays verbatim as
|
|
84
|
+
* the response body, and an older Claude Code's `Claude AI usage limit reached|1781085600`.
|
|
85
|
+
*/
|
|
86
|
+
const structuredResetEpochRegex = /"resets_at"\s*:\s*(\d{10})(?![\d.])|\blimit reached\|(\d{10})(?![\d.])/i;
|
|
87
|
+
/** The same body's wait until that reset in seconds, `"resets_in_seconds":9000` — what an older body carries instead. */
|
|
88
|
+
const structuredResetWaitRegex = /"resets_in_seconds"\s*:\s*(\d+)(?![\d.])/i;
|
|
89
|
+
/** One part of a relative reset — `2 hours`, `30m`, `1 day`, `3s`, `40.15s` — closed by a non-letter (`1h30m` has no word break). */
|
|
90
|
+
const relativeResetPartSource = String.raw `(\d+(?:\.\d+)?)\s*(d|days?|h|hours?|m|mins?|minutes?|s|secs?|seconds?)(?![a-z])`;
|
|
91
|
+
/**
|
|
92
|
+
* Matches a relative reset — "resets in 2h", "in 45m", "in 90 min", "reset after 8h27m3s" — and every part that follows
|
|
93
|
+
* it: OpenCode Go writes "It will reset in 2 hours 30 minutes" / "in 1 day 3 hours"; Google's subscription quotas
|
|
94
|
+
* "Your quota will reset after 2s" / "after 146h52m11s".
|
|
95
|
+
*/
|
|
96
|
+
const relativeResetRegex = new RegExp(String.raw `\b(?:in|after)\s+((?:${relativeResetPartSource}(?:\s*(?:,|and)?\s*))+)`, 'i');
|
|
97
|
+
const relativeResetPartRegex = new RegExp(relativeResetPartSource, 'gi');
|
|
60
98
|
/**
|
|
61
99
|
* Matches absolute clock phrasings after a reset trigger word. The trigger is
|
|
62
100
|
* either `at` (`resets at 3pm`, `at 15:00`) OR a BARE `reset`/`resets` — Claude's
|
|
@@ -73,8 +111,22 @@ const clockResetRegex = /\b(?:reset(?:s|ting)?|at)\s+(\d{1,2})(?!\d)(?::(\d{2}))
|
|
|
73
111
|
* `Zoom`) is not read as the Zulu zone.
|
|
74
112
|
*/
|
|
75
113
|
const trailingZoneRegex = /^\s*\(?\s*(UTC|GMT|Z|[+-]\d{2}:?\d{2})\b\s*\)?/i;
|
|
76
|
-
/**
|
|
77
|
-
|
|
114
|
+
/**
|
|
115
|
+
* A NAMED zone in parentheses right after the clock — Claude Code formats the reset in its own zone and says which:
|
|
116
|
+
* `resets 2:10pm (Europe/Moscow)`. An IANA name (`Area/Location`, possibly deeper); validated by `Intl` before use.
|
|
117
|
+
*/
|
|
118
|
+
const trailingNamedZoneRegex = /^\s*\(\s*([A-Za-z]+(?:\/[A-Za-z0-9_+-]+)+)\s*\)/;
|
|
119
|
+
/**
|
|
120
|
+
* Matches a DATED reset — a window more than a day ahead, written by Claude Code as `toLocaleString('en-US', { month:
|
|
121
|
+
* 'short', day: 'numeric', hour: 'numeric' })`: `resets Oct 25, 3pm`, `resets Oct 25, 3:30pm`, `resets Jan 2, 2027,
|
|
122
|
+
* 3pm` (the year only when it is not this one); claude.ai's `It resets Oct 25 at 3pm` too. Tried BEFORE the bare clock,
|
|
123
|
+
* whose `at 3pm` would otherwise win and drop the date.
|
|
124
|
+
*/
|
|
125
|
+
const monthDayResetRegex = /\breset(?:s|ting)?\s+(?:on\s+)?(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\.?\s+(\d{1,2})(?:,\s*(\d{4}))?(?:\s*,\s*|\s+at\s+|\s+)(\d{1,2})(?::(\d{2}))?\s*(am|pm)\b/i;
|
|
126
|
+
/** The month abbreviations {@link monthDayResetRegex} captures, in calendar order. */
|
|
127
|
+
const monthAbbreviations = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
|
|
128
|
+
/** Matches an ISO-8601 timestamp anywhere in the text: its date and clock, and its zone when it carries one. */
|
|
129
|
+
const isoTimestampRegex = /\b(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::\d{2})?(?:\.\d+)?(Z|[+-]\d{2}:?\d{2})?\b/;
|
|
78
130
|
const hoursPerDay = 24;
|
|
79
131
|
const minutesPerHour = 60;
|
|
80
132
|
const msPerDay = hoursPerDay * msPerHour;
|
|
@@ -96,7 +148,8 @@ exports.authErrorPhrasesRe = /please run \/login|not logged in|invalid authentic
|
|
|
96
148
|
*/
|
|
97
149
|
const usageWindowPhrases = [
|
|
98
150
|
String.raw `(?:session|usage|weekly|daily|monthly|hourly|\d+-hours?)[\s_]+limit`,
|
|
99
|
-
|
|
151
|
+
// "exceeded your RATE limit" / "your CONCURRENCY limit" is a throttle, not a window.
|
|
152
|
+
String.raw `(?:hit|reached|exceeded)\s+your\s+(?:(?!rate\b|concurren)[\w-]+\s+){0,3}limit`,
|
|
100
153
|
String.raw `credit balance (?:is )?too low`,
|
|
101
154
|
String.raw `out of (?:credits|usage)`,
|
|
102
155
|
String.raw `quota`,
|
|
@@ -114,9 +167,35 @@ const exhaustedQuotaCodesRe = /insufficient_quota|exceeded your current quota|us
|
|
|
114
167
|
/**
|
|
115
168
|
* Observed subscription-window wording ("This request would exceed your account's rate limit. Please try
|
|
116
169
|
* again later."); do not generalize it to every bare rate limit. Keyed on the distinctive head only, so a
|
|
117
|
-
* relay that truncates the tail or types a curly apostrophe still matches.
|
|
170
|
+
* relay that truncates the tail or types a curly apostrophe still matches. UNCONFIRMED on its own: the same
|
|
171
|
+
* 429 comes at 0–84 % shown usage (claude-code#19673) — see {@link getUnconfirmedLimitAction}.
|
|
118
172
|
*/
|
|
119
173
|
const subscriptionLimitMessageRe = /would exceed your account['\u2019]?s rate limit/i;
|
|
174
|
+
/**
|
|
175
|
+
* Markers of a SHORT-window throttle — per minute / per second (spelled out, or in a Google quota id such as
|
|
176
|
+
* `GenerateRequestsPerMinutePerProjectPerModel`), RPM / TPM, concurrency, a retry hint in seconds (`Please retry in
|
|
177
|
+
* 40.15s`, Google's `"retryDelay": "40s"`), and Google's bare `Resource (has been) exhausted`. Such a limit clears by
|
|
178
|
+
* the next retry, so it is transient whatever quota vocabulary rides along: Google answers a used-up per-MINUTE quota
|
|
179
|
+
* with the same 429 RESOURCE_EXHAUSTED and "You exceeded your current quota" as a per-DAY one.
|
|
180
|
+
*/
|
|
181
|
+
const shortWindowThrottleRe = /\bper[\s_-]?(?:minute|min|second|sec)\b|per(?:minute|second)|\b(?:rpm|tpm|rps|tps|qpm|qps)\b|\bconcurren(?:t|cy)\b|\b(?:retry|try again)\s+(?:in|after)\s+\d+(?:\.\d+)?\s*(?:ms|s|secs?|seconds?)\b|"retryDelay"\s*:\s*"\d+(?:\.\d+)?s"|\bresource[\s_]+(?:has\s+been\s+)?exhausted\b/i;
|
|
182
|
+
/**
|
|
183
|
+
* Markers of a LONG window — a day, a week, a month (spelled out, in a quota id such as `…PerDayPerProjectPerModel`,
|
|
184
|
+
* or as RPD / TPD), a session / N-hour / usage window, a used-up balance. Where one is named, a short retry hint beside
|
|
185
|
+
* it (Google's RetryInfo comes with a daily quota too) does not make the limit transient.
|
|
186
|
+
*/
|
|
187
|
+
const longWindowRe = /\bper[\s_-]?(?:day|week|month)\b|per(?:day|week|month)|\b(?:rpd|tpd)\b|\b(?:daily|weekly|monthly)\b|\b(?:session|usage|hourly|\d+-hours?)[\s_]+limit|usage_limit_reached|insufficient_quota|credit balance|out of (?:credits|usage)|billing\s+(?:hard\s+)?limit/i;
|
|
188
|
+
/**
|
|
189
|
+
* That a text REPORTS a limit being hit rather than describing one — "the API limit is 5 requests per minute" is prose.
|
|
190
|
+
* The short-window rule needs it beside {@link shortWindowThrottleRe}.
|
|
191
|
+
*/
|
|
192
|
+
const limitHitRe = /\b(?:exceeded|exhausted|reached|hit your|too many|throttl\w*|retry|try again)\b/i;
|
|
193
|
+
/**
|
|
194
|
+
* Gemini's refusal of a request whose `contents` end on a `model` turn (a 400 INVALID_ARGUMENT with no code of its
|
|
195
|
+
* own). OpenCode v1's agent loop sends one after a step that finished `unknown` or on its max-steps prompt (upstream
|
|
196
|
+
* fixed it in v2 only); the bot's prompts are always user turns, and its "continue" is one.
|
|
197
|
+
*/
|
|
198
|
+
const conversationTailRe = /requests ending with a model turn are not supported/i;
|
|
120
199
|
/** Throttling markers that are transient whatever else the text says (the live "not your usage limit" trap). */
|
|
121
200
|
const transientMarkersRe = /rate.?limited|temporarily limiting requests|too many requests|overloaded|\b(429|503|529)\b/i;
|
|
122
201
|
/** A bare "rate limit" (no "-ed"): transient only when no explicit usage-window wording rides along. */
|
|
@@ -137,6 +216,27 @@ exports.usageLimitPhraseSource = [
|
|
|
137
216
|
exports.usageLimitPhrasesRe = new RegExp(exports.usageLimitPhraseSource, 'i');
|
|
138
217
|
/** {@link usageWindowPhrases} compiled — the {@link nonUsageLimitRe} exemption. */
|
|
139
218
|
const usageWindowPhrasesRe = new RegExp(usageWindowPhrases.join('|'), 'i');
|
|
219
|
+
/**
|
|
220
|
+
* Claude Code's NON-blocking usage notices: the `allowed_warning` status
|
|
221
|
+
* (`Approaching session limit · resets 11:29pm`, `You've used 90% of your weekly
|
|
222
|
+
* limit · resets …`, `You're close to your usage limit`) and the switch onto paid
|
|
223
|
+
* extra usage (`5-hour limit resets 11:29pm - continuing with usage credits`,
|
|
224
|
+
* `Now using extra usage`). They name the window and its reset exactly like the
|
|
225
|
+
* block does (`You've hit your session limit · resets …`, status `rejected`), but
|
|
226
|
+
* the request went through — read as a limit, they stopped a working topic until
|
|
227
|
+
* the reset. The warning lead-ins are anchored at the text start (after any glyph),
|
|
228
|
+
* the way the CLI renders them; the extra-usage wordings may sit anywhere.
|
|
229
|
+
*/
|
|
230
|
+
const usageLimitWarningRe = /^\W*(?:approaching\b|you['\u2019]ve used \d+%|you['\u2019]re close to\b)|continuing with (?:usage|extra) (?:credits|usage)|\b(?:now )?using (?:usage credits|extra usage|credits|your usage allocation)\b/i;
|
|
231
|
+
/**
|
|
232
|
+
* @description Whether a text is a usage WARNING (see {@link usageLimitWarningRe})
|
|
233
|
+
* — the account is still allowed, so it is never a usage-limit error. Shared with
|
|
234
|
+
* the Claude scrape detector (`getClaudeAgentErrorLine`), which skips such a row so
|
|
235
|
+
* a real block further down the same pane delta is still found.
|
|
236
|
+
*/
|
|
237
|
+
function checkIsUsageLimitWarningText(text) {
|
|
238
|
+
return usageLimitWarningRe.test(text);
|
|
239
|
+
}
|
|
140
240
|
/**
|
|
141
241
|
* A limit that WAITING can never clear: a context / token / prompt-length overflow
|
|
142
242
|
* is fixed by sending less, not by sitting out a window. The real provider wording
|
|
@@ -184,8 +284,18 @@ function checkIsUsageLimitText(text) {
|
|
|
184
284
|
* 1. auth / logged out (login, bad credentials) → `{ kind: 'auth' }`. Must win:
|
|
185
285
|
* a wait never fixes these (they need a re-login or server restart), so they
|
|
186
286
|
* are SURFACED (a pinned notice), never auto-retried. Used to return `null`.
|
|
287
|
+
* 1a. the refusal of a history that ends on the model's turn ({@link conversationTailRe}) → `conversationTail`.
|
|
288
|
+
* 1b. a usage WARNING ({@link checkIsUsageLimitWarningText}) → `null`: the
|
|
289
|
+
* request went through; it names a window and a reset like the block does.
|
|
290
|
+
* 1b'. a stated reset ({@link parseResetAt}) within {@link shortStatedResetMaxMs} → `transient`;
|
|
291
|
+
* a longer one keeps 1c from calling the limit short. A short one a RETRY verb states ("retry in 40s") that reaches
|
|
292
|
+
* the usage-limit branches below (a long window named beside it) is a retry hint: their `resetAt` drops it.
|
|
293
|
+
* 1c. a SHORT-window throttle ({@link shortWindowThrottleRe}, reported as hit —
|
|
294
|
+
* {@link limitHitRe}) that names no long
|
|
295
|
+
* window ({@link longWindowRe}) → `transient`: per-minute quotas, concurrency,
|
|
296
|
+
* "retry in 40s" — ahead of the quota codes, which they share with daily quotas.
|
|
187
297
|
* 2. an exhausted-quota provider CODE ({@link exhaustedQuotaCodesRe}) or the observed
|
|
188
|
-
* subscription wording ({@link subscriptionLimitMessageRe}) → `usageLimit`.
|
|
298
|
+
* subscription wording ({@link subscriptionLimitMessageRe}, flagged `isUnconfirmed`) → `usageLimit`.
|
|
189
299
|
* Ahead of transient because these ride a 429 (OpenAI `insufficient_quota`,
|
|
190
300
|
* Google `RESOURCE_EXHAUSTED`), which the next branch would read as a hiccup.
|
|
191
301
|
* 3. transient (rate-limited, overloaded, 429/503/529, or a bare "rate limit" without an explicit
|
|
@@ -204,8 +314,33 @@ function classifyAgentApiError(text, now) {
|
|
|
204
314
|
if (exports.authErrorPhrasesRe.test(text)) {
|
|
205
315
|
return { kind: 'auth' };
|
|
206
316
|
}
|
|
207
|
-
if (
|
|
208
|
-
return { kind: '
|
|
317
|
+
if (conversationTailRe.test(text))
|
|
318
|
+
return { kind: 'conversationTail' };
|
|
319
|
+
if (checkIsUsageLimitWarningText(text))
|
|
320
|
+
return null;
|
|
321
|
+
// The reset the provider states decides a limit's length when it names one: within the first transient retry it is a
|
|
322
|
+
// throttle, beyond it a window — whatever short-window code (RESOURCE_EXHAUSTED) or quota words ride along. A NAMED
|
|
323
|
+
// long window (a daily quota id beside Google's "retry in 40s") still wins, and a reset already behind `now` decides
|
|
324
|
+
// nothing here (a live error naming one is still announced).
|
|
325
|
+
const statedResetAt = parseResetAt(text, now);
|
|
326
|
+
const statedResetInMs = statedResetAt === undefined ? null : statedResetAt - now;
|
|
327
|
+
const isShortStatedReset = statedResetAt !== undefined && checkIsShortStatedReset(statedResetAt, now);
|
|
328
|
+
const isLongStatedReset = statedResetInMs !== null && statedResetInMs > exports.shortStatedResetMaxMs;
|
|
329
|
+
const isLongWindowNamed = longWindowRe.test(text);
|
|
330
|
+
if (isShortStatedReset && limitHitRe.test(text) && !isLongWindowNamed)
|
|
331
|
+
return { kind: 'transient' };
|
|
332
|
+
if (shortWindowThrottleRe.test(text) && limitHitRe.test(text) && !isLongWindowNamed && !isLongStatedReset)
|
|
333
|
+
return { kind: 'transient' };
|
|
334
|
+
// Past this point it is a usage limit, and a SHORT reset a RETRY verb states beside one is a retry hint, not the
|
|
335
|
+
// window's end: Google sends a spent daily quota with the same "Please retry in 40.15s" as a per-minute one, and taken
|
|
336
|
+
// as the reset it set the wait at ~100 s and spent the attempts in minutes. What a "reset(s) in / after" or a clock
|
|
337
|
+
// states is the window's own end, however close.
|
|
338
|
+
const limitResetAt = isShortStatedReset && checkIsRetryHintReset(text, now) ? undefined : statedResetAt;
|
|
339
|
+
if (exhaustedQuotaCodesRe.test(text)) {
|
|
340
|
+
return { kind: 'usageLimit', resetAt: limitResetAt };
|
|
341
|
+
}
|
|
342
|
+
if (subscriptionLimitMessageRe.test(text)) {
|
|
343
|
+
return { kind: 'usageLimit', resetAt: limitResetAt, isUnconfirmed: true };
|
|
209
344
|
}
|
|
210
345
|
// The observed subscription wording is handled above, including a 429 wrapper.
|
|
211
346
|
// Ordinary rate-limit errors must not fall into the generic limit + retry-hint branch,
|
|
@@ -214,22 +349,66 @@ function classifyAgentApiError(text, now) {
|
|
|
214
349
|
return { kind: 'transient' };
|
|
215
350
|
}
|
|
216
351
|
if (checkIsUsageLimitText(text)) {
|
|
217
|
-
return { kind: 'usageLimit', resetAt:
|
|
352
|
+
return { kind: 'usageLimit', resetAt: limitResetAt };
|
|
218
353
|
}
|
|
219
354
|
return null;
|
|
220
355
|
}
|
|
356
|
+
/** A retry verb right before a relative reset: "Please retry in 40s", "try again after 30s". */
|
|
357
|
+
const retryVerbBeforeResetRe = /\b(?:retry|try again)\s*$/i;
|
|
358
|
+
/**
|
|
359
|
+
* @description Whether the reset {@link parseResetAt} reads in `text` is a retry hint — a relative one stated by a retry
|
|
360
|
+
* verb — rather than a reset ("Your quota will reset after 4m30s", "resets in 2 minutes"). A machine-form reset is read
|
|
361
|
+
* ahead of the relative shape and is the window's own end, whatever retry wording rides beside it.
|
|
362
|
+
*/
|
|
363
|
+
function checkIsRetryHintReset(text, now) {
|
|
364
|
+
if (parseStructuredResetAt(text, now) !== undefined)
|
|
365
|
+
return false;
|
|
366
|
+
const match = relativeResetRegex.exec(text);
|
|
367
|
+
return match !== null && retryVerbBeforeResetRe.test(text.slice(0, match.index));
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* The short retry an UNCONFIRMED usage limit ({@link AgentApiErrorClass.isUnconfirmed}) gets first: long enough not to
|
|
371
|
+
* hammer an account that is really limited, short enough that a refused single request costs the topic two minutes, not
|
|
372
|
+
* the hours to the window's reset.
|
|
373
|
+
*/
|
|
374
|
+
exports.unconfirmedLimitProbeDelayMs = 2 * msPerMinute;
|
|
375
|
+
/**
|
|
376
|
+
* @description What an UNCONFIRMED usage limit does: `probe` — a short retry ({@link unconfirmedLimitProbeDelayMs}),
|
|
377
|
+
* no limit notice; `waitForReset` — the usual usage-limit wait, once the limit is confirmed: it repeated right after
|
|
378
|
+
* the probe (`isRepeatAfterProbe`), or the account's usage reading shows a window at 100 % (`isUsageExhausted`).
|
|
379
|
+
*/
|
|
380
|
+
function getUnconfirmedLimitAction(input) {
|
|
381
|
+
return input.isRepeatAfterProbe || input.isUsageExhausted ? 'waitForReset' : 'probe';
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* @description Whether a usage-limit error names a reset that is already behind
|
|
385
|
+
* `now`: the window is over. The boot recovery of a parked error (read against the
|
|
386
|
+
* time it was written) uses it to resume without announcing a limit; a live error
|
|
387
|
+
* is announced whatever its reset says.
|
|
388
|
+
*/
|
|
389
|
+
function checkIsUsageLimitWindowOver(cls, now) {
|
|
390
|
+
return cls.kind === 'usageLimit' && cls.resetAt !== undefined && cls.resetAt <= now;
|
|
391
|
+
}
|
|
221
392
|
/**
|
|
222
393
|
* @description Best-effort parse of a "reset time" out of a usage-limit message.
|
|
223
394
|
* Never throws: any unparseable / absent time yields `undefined`, which the
|
|
224
|
-
* caller treats as "use the fixed delay". Handles
|
|
225
|
-
* -
|
|
395
|
+
* caller treats as "use the fixed delay". Handles five shapes, in order:
|
|
396
|
+
* - machine form: an epoch (`"resets_at":…` in a relayed ChatGPT body, Claude's
|
|
397
|
+
* `limit reached|<epoch>`) or that body's `"resets_in_seconds":…` — exact, so
|
|
398
|
+
* first ({@link parseStructuredResetAt}).
|
|
399
|
+
* - relative: "resets in 2h" / "in 45m" / "in 2 hours 30 minutes" → `now` + the
|
|
400
|
+
* sum of the parts.
|
|
401
|
+
* - dated: "resets Oct 25, 3pm (Europe/Moscow)" — a weekly window, more than a
|
|
402
|
+
* day ahead → that date and clock, in the stated zone (or the local one).
|
|
226
403
|
* - absolute clock: "resets at 3pm" / "at 15:00" / "resets 10:50pm (UTC)" → the
|
|
227
404
|
* NEXT occurrence of that clock time at or after `now`. An EXPLICIT zone
|
|
228
|
-
* suffix
|
|
229
|
-
*
|
|
405
|
+
* suffix — an offset (`(UTC)`, `+05:00`) or a named zone (`(Europe/Moscow)`,
|
|
406
|
+
* the way Claude Code writes it) — is load-bearing: the clock is then resolved
|
|
407
|
+
* IN THAT ZONE, not in the instance/host zone. Without it a `10:50pm (UTC)` reset on a `+04:00` box
|
|
230
408
|
* resolved four hours EARLY — the retry fires while the limit still holds,
|
|
231
409
|
* re-errors, and burns one of the six attempts for nothing.
|
|
232
|
-
* - ISO timestamp anywhere in the text →
|
|
410
|
+
* - ISO timestamp anywhere in the text → that instant; a zone-less one only with
|
|
411
|
+
* a zone right after it (see {@link resolveIsoResetTime}).
|
|
233
412
|
*
|
|
234
413
|
* A clock shape that fails to resolve (out-of-range hour) falls THROUGH to the
|
|
235
414
|
* ISO branch rather than giving up, so a message carrying both still parses.
|
|
@@ -238,12 +417,24 @@ function classifyAgentApiError(text, now) {
|
|
|
238
417
|
* the next occurrence of an absolute clock time.
|
|
239
418
|
*/
|
|
240
419
|
function parseResetAt(text, now) {
|
|
420
|
+
const structuredResetAt = parseStructuredResetAt(text, now);
|
|
421
|
+
if (structuredResetAt !== undefined)
|
|
422
|
+
return structuredResetAt;
|
|
241
423
|
const relativeMatch = relativeResetRegex.exec(text);
|
|
242
424
|
if (relativeMatch) {
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
425
|
+
let durationMs = 0;
|
|
426
|
+
for (const part of relativeMatch[1].matchAll(relativeResetPartRegex)) {
|
|
427
|
+
const amount = Number.parseFloat(part[1]);
|
|
428
|
+
const unit = part[2].toLowerCase();
|
|
429
|
+
durationMs += amount * (unit.startsWith('d') ? msPerDay : unit.startsWith('h') ? msPerHour : unit.startsWith('m') ? msPerMinute : msPerSecond);
|
|
430
|
+
}
|
|
431
|
+
return now + Math.round(durationMs);
|
|
432
|
+
}
|
|
433
|
+
const monthDayMatch = monthDayResetRegex.exec(text);
|
|
434
|
+
if (monthDayMatch) {
|
|
435
|
+
const resolved = resolveMonthDayResetTime(monthDayMatch, text, now);
|
|
436
|
+
if (resolved !== undefined)
|
|
437
|
+
return resolved;
|
|
247
438
|
}
|
|
248
439
|
const clockMatch = clockResetRegex.exec(text);
|
|
249
440
|
if (clockMatch) {
|
|
@@ -252,13 +443,24 @@ function parseResetAt(text, now) {
|
|
|
252
443
|
return resolved;
|
|
253
444
|
}
|
|
254
445
|
const isoMatch = isoTimestampRegex.exec(text);
|
|
255
|
-
if (isoMatch)
|
|
256
|
-
|
|
257
|
-
if (!Number.isNaN(parsed))
|
|
258
|
-
return parsed;
|
|
259
|
-
}
|
|
446
|
+
if (isoMatch)
|
|
447
|
+
return resolveIsoResetTime(isoMatch, text);
|
|
260
448
|
return undefined;
|
|
261
449
|
}
|
|
450
|
+
/**
|
|
451
|
+
* The machine-form reset ({@link structuredResetEpochRegex}, {@link structuredResetWaitRegex}) as epoch ms, or
|
|
452
|
+
* `undefined` when there is none. An epoch not ahead of `now` is no reset, nor is a zero wait: a stale `|<epoch>` line or
|
|
453
|
+
* a clock ahead of the provider's would arm a zero delay and spend every attempt in seconds. (The boot recovery reads
|
|
454
|
+
* against the time the error was written, so the reset it judges over is still read.)
|
|
455
|
+
*/
|
|
456
|
+
function parseStructuredResetAt(text, now) {
|
|
457
|
+
const epochMatch = structuredResetEpochRegex.exec(text);
|
|
458
|
+
const epochMs = epochMatch ? Number.parseInt(epochMatch[1] ?? epochMatch[2], 10) * msPerSecond : 0;
|
|
459
|
+
if (epochMs > now)
|
|
460
|
+
return epochMs;
|
|
461
|
+
const waitSeconds = Number.parseInt(structuredResetWaitRegex.exec(text)?.[1] ?? '', 10);
|
|
462
|
+
return waitSeconds > 0 ? now + waitSeconds * msPerSecond : undefined;
|
|
463
|
+
}
|
|
262
464
|
/**
|
|
263
465
|
* Read an explicit zone token to an offset from UTC in ms, or `undefined` when it
|
|
264
466
|
* is not a zone we can resolve arithmetically. `Z`/`UTC`/`GMT` are zero;
|
|
@@ -278,6 +480,79 @@ function getZoneOffsetMs(zone) {
|
|
|
278
480
|
const sign = match[1] === '-' ? -1 : 1;
|
|
279
481
|
return sign * (hours * msPerHour + minutes * msPerMinute);
|
|
280
482
|
}
|
|
483
|
+
/** @description A 12-hour clock (`3`, `pm`) as the hour of a 24-hour day; a bare hour is taken as written. */
|
|
484
|
+
function getHourOfDay(rawHour, meridiem) {
|
|
485
|
+
const lowerMeridiem = meridiem?.toLowerCase();
|
|
486
|
+
if (lowerMeridiem === 'pm' && rawHour < 12)
|
|
487
|
+
return rawHour + 12;
|
|
488
|
+
if (lowerMeridiem === 'am' && rawHour === 12)
|
|
489
|
+
return 0;
|
|
490
|
+
return rawHour;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Resolve a dated reset ({@link monthDayResetRegex}) to its epoch ms: in the zone named or offset right after it, else
|
|
494
|
+
* in the process-local zone. Without a written year it is this year's date — or next year's once this year's has passed
|
|
495
|
+
* (a reset is always ahead). `undefined` for an out-of-range day or clock.
|
|
496
|
+
*/
|
|
497
|
+
function resolveMonthDayResetTime(match, text, now) {
|
|
498
|
+
const month = monthAbbreviations.indexOf(match[1].toLowerCase()) + 1;
|
|
499
|
+
const day = Number.parseInt(match[2], 10);
|
|
500
|
+
const writtenYear = match[3] ? Number.parseInt(match[3], 10) : null;
|
|
501
|
+
const hour = getHourOfDay(Number.parseInt(match[4], 10), match[6]);
|
|
502
|
+
const minute = match[5] ? Number.parseInt(match[5], 10) : 0;
|
|
503
|
+
if (day < 1 || day > 31 || hour >= hoursPerDay || minute >= minutesPerHour)
|
|
504
|
+
return undefined;
|
|
505
|
+
const textAfterClock = text.slice(match.index + match[0].length);
|
|
506
|
+
const namedZone = getTrailingNamedZone(textAfterClock);
|
|
507
|
+
const zoneMatch = namedZone === null ? trailingZoneRegex.exec(textAfterClock) : null;
|
|
508
|
+
const offsetMs = zoneMatch ? getZoneOffsetMs(zoneMatch[1]) : undefined;
|
|
509
|
+
const resolveInYear = (year) => {
|
|
510
|
+
if (namedZone !== null)
|
|
511
|
+
return (0, timezone_1.getEpochMsOfZonedWallClock)(namedZone, { year, month, day, hour, minute });
|
|
512
|
+
if (offsetMs !== undefined)
|
|
513
|
+
return Date.UTC(year, month - 1, day, hour, minute) - offsetMs;
|
|
514
|
+
return new Date(year, month - 1, day, hour, minute).getTime();
|
|
515
|
+
};
|
|
516
|
+
const currentYear = namedZone !== null ? (0, timezone_1.getZonedWallClock)(namedZone, now).year : new Date(now).getFullYear();
|
|
517
|
+
const resolved = resolveInYear(writtenYear ?? currentYear);
|
|
518
|
+
return writtenYear === null && resolved < now ? resolveInYear(currentYear + 1) : resolved;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Resolve an ISO stamp ({@link isoTimestampRegex}) to epoch ms. One with its own zone is that instant. A ZONE-LESS one
|
|
522
|
+
* says nothing about its zone (RFC 3339 requires an offset, and no provider documents a zone-less reset): read in the
|
|
523
|
+
* bot's own zone it could be hours off either way, so it counts only with a zone right after it (`… 23:29 UTC`,
|
|
524
|
+
* `… (Europe/Moscow)`) — else `undefined`, an unknown reset (the fixed delay, or OpenCode's own Retry-After), not a guess.
|
|
525
|
+
*/
|
|
526
|
+
function resolveIsoResetTime(match, text) {
|
|
527
|
+
if (match[6] !== undefined) {
|
|
528
|
+
const parsed = Date.parse(match[0]);
|
|
529
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
530
|
+
}
|
|
531
|
+
const [year, month, day, hour, minute] = match.slice(1, 6).map((part) => Number.parseInt(part, 10));
|
|
532
|
+
const textAfterStamp = text.slice(match.index + match[0].length);
|
|
533
|
+
const namedZone = getTrailingNamedZone(textAfterStamp);
|
|
534
|
+
if (namedZone !== null)
|
|
535
|
+
return (0, timezone_1.getEpochMsOfZonedWallClock)(namedZone, { year, month, day, hour, minute });
|
|
536
|
+
const zoneMatch = trailingZoneRegex.exec(textAfterStamp);
|
|
537
|
+
const offsetMs = zoneMatch ? getZoneOffsetMs(zoneMatch[1]) : undefined;
|
|
538
|
+
return offsetMs === undefined ? undefined : Date.UTC(year, month - 1, day, hour, minute) - offsetMs;
|
|
539
|
+
}
|
|
540
|
+
/** The IANA zone named in parentheses at the start of `textAfterClock`, canonicalized; `null` when none or unknown. */
|
|
541
|
+
function getTrailingNamedZone(textAfterClock) {
|
|
542
|
+
const zoneName = trailingNamedZoneRegex.exec(textAfterClock)?.[1];
|
|
543
|
+
return zoneName === undefined ? null : (0, timezone_1.getCanonicalTimezone)(zoneName);
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* The next instant at or after `now` at which `timezone` reads `hour:minute`: today's in that zone, or tomorrow's
|
|
547
|
+
* once today's has passed. Daylight saving is the zone's own (`Intl`), not a fixed offset.
|
|
548
|
+
*/
|
|
549
|
+
function resolveNextZonedClockTime(timezone, hour, minute, now) {
|
|
550
|
+
const today = (0, timezone_1.getZonedWallClock)(timezone, now);
|
|
551
|
+
const todayMs = (0, timezone_1.getEpochMsOfZonedWallClock)(timezone, { year: today.year, month: today.month, day: today.day, hour, minute });
|
|
552
|
+
if (todayMs >= now)
|
|
553
|
+
return todayMs;
|
|
554
|
+
return (0, timezone_1.getEpochMsOfZonedWallClock)(timezone, { year: today.year, month: today.month, day: today.day + 1, hour, minute });
|
|
555
|
+
}
|
|
281
556
|
/**
|
|
282
557
|
* Resolve a clock match to the next epoch ms at or after `now` that lands on that
|
|
283
558
|
* wall-clock time. When the text carries an explicit zone right after the clock
|
|
@@ -286,18 +561,16 @@ function getZoneOffsetMs(zone) {
|
|
|
286
561
|
* Returns `undefined` for an out-of-range hour/minute rather than guessing.
|
|
287
562
|
*/
|
|
288
563
|
function resolveNextClockTime(match, text, now) {
|
|
289
|
-
const rawHour = Number.parseInt(match[1], 10);
|
|
290
564
|
const minute = match[2] ? Number.parseInt(match[2], 10) : 0;
|
|
291
|
-
const
|
|
292
|
-
let hour = rawHour;
|
|
293
|
-
if (meridiem === 'pm' && rawHour < 12)
|
|
294
|
-
hour = rawHour + 12;
|
|
295
|
-
else if (meridiem === 'am' && rawHour === 12)
|
|
296
|
-
hour = 0;
|
|
565
|
+
const hour = getHourOfDay(Number.parseInt(match[1], 10), match[3]);
|
|
297
566
|
if (hour < 0 || hour >= hoursPerDay || minute < 0 || minute >= minutesPerHour) {
|
|
298
567
|
return undefined;
|
|
299
568
|
}
|
|
300
|
-
const
|
|
569
|
+
const textAfterClock = text.slice(match.index + match[0].length);
|
|
570
|
+
const namedZone = getTrailingNamedZone(textAfterClock);
|
|
571
|
+
if (namedZone !== null)
|
|
572
|
+
return resolveNextZonedClockTime(namedZone, hour, minute, now);
|
|
573
|
+
const zoneMatch = trailingZoneRegex.exec(textAfterClock);
|
|
301
574
|
const offsetMs = zoneMatch ? getZoneOffsetMs(zoneMatch[1]) : undefined;
|
|
302
575
|
if (offsetMs !== undefined) {
|
|
303
576
|
// Shift onto the zone's timeline, snap to that zone's midnight, then shift
|
|
@@ -326,6 +599,8 @@ function resolveNextClockTime(match, text, now) {
|
|
|
326
599
|
* time never overflows the timer.
|
|
327
600
|
* - transient: {@link transientBackoffMinutes}[attempt-1]; giveUp once attempt
|
|
328
601
|
* exceeds {@link transientMaxAttempts}.
|
|
602
|
+
* - conversationTail: {@link conversationTailRetryDelayMs}; giveUp once attempt exceeds
|
|
603
|
+
* {@link conversationTailMaxAttempts}.
|
|
329
604
|
* - usageLimit: `resetAt` present → `resetAt - now + resetBufferMs`; else
|
|
330
605
|
* {@link usageLimitDefaultMs}; giveUp once attempt exceeds
|
|
331
606
|
* {@link usageLimitMaxAttempts}.
|
|
@@ -335,6 +610,9 @@ function getRetryPlan(args) {
|
|
|
335
610
|
// reached); guard so it can never fall through into the usageLimit branch.
|
|
336
611
|
if (args.kind === 'auth')
|
|
337
612
|
return { giveUp: true };
|
|
613
|
+
if (args.kind === 'conversationTail') {
|
|
614
|
+
return args.attempt > exports.conversationTailMaxAttempts ? { giveUp: true } : { delayMs: exports.conversationTailRetryDelayMs };
|
|
615
|
+
}
|
|
338
616
|
if (args.kind === 'transient') {
|
|
339
617
|
if (args.attempt > exports.transientMaxAttempts)
|
|
340
618
|
return { giveUp: true };
|