charness 3.2.0 → 3.3.1

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.
Files changed (313) hide show
  1. package/.env.example +3 -6
  2. package/DEVELOPMENT.md +15 -0
  3. package/README.md +111 -38
  4. package/SETUP.md +121 -16
  5. package/dist/adapters/adapterEventRouting.d.ts +2 -1
  6. package/dist/adapters/adapterEventRouting.d.ts.map +1 -1
  7. package/dist/adapters/adapterEventRouting.js +3 -0
  8. package/dist/adapters/adapterEventRouting.js.map +1 -1
  9. package/dist/adapters/adapterNames.d.ts +13 -0
  10. package/dist/adapters/adapterNames.d.ts.map +1 -1
  11. package/dist/adapters/adapterNames.js +16 -0
  12. package/dist/adapters/adapterNames.js.map +1 -1
  13. package/dist/adapters/backendUnavailableError.d.ts +11 -0
  14. package/dist/adapters/backendUnavailableError.d.ts.map +1 -0
  15. package/dist/adapters/backendUnavailableError.js +19 -0
  16. package/dist/adapters/backendUnavailableError.js.map +1 -0
  17. package/dist/adapters/claudeCliAdapter.d.ts +29 -6
  18. package/dist/adapters/claudeCliAdapter.d.ts.map +1 -1
  19. package/dist/adapters/claudeCliAdapter.js +70 -16
  20. package/dist/adapters/claudeCliAdapter.js.map +1 -1
  21. package/dist/adapters/claudeJsonStreamAdapter.d.ts +36 -3
  22. package/dist/adapters/claudeJsonStreamAdapter.d.ts.map +1 -1
  23. package/dist/adapters/claudeJsonStreamAdapter.js +93 -30
  24. package/dist/adapters/claudeJsonStreamAdapter.js.map +1 -1
  25. package/dist/adapters/createAdapter.d.ts +3 -14
  26. package/dist/adapters/createAdapter.d.ts.map +1 -1
  27. package/dist/adapters/createAdapter.js +9 -19
  28. package/dist/adapters/createAdapter.js.map +1 -1
  29. package/dist/adapters/openCodeAdapter.d.ts +140 -12
  30. package/dist/adapters/openCodeAdapter.d.ts.map +1 -1
  31. package/dist/adapters/openCodeAdapter.js +385 -119
  32. package/dist/adapters/openCodeAdapter.js.map +1 -1
  33. package/dist/apiErrorRetry.d.ts +70 -6
  34. package/dist/apiErrorRetry.d.ts.map +1 -1
  35. package/dist/apiErrorRetry.js +312 -34
  36. package/dist/apiErrorRetry.js.map +1 -1
  37. package/dist/apiRetryKick.d.ts +26 -1
  38. package/dist/apiRetryKick.d.ts.map +1 -1
  39. package/dist/apiRetryKick.js +41 -2
  40. package/dist/apiRetryKick.js.map +1 -1
  41. package/dist/bot.d.ts +2 -1
  42. package/dist/bot.d.ts.map +1 -1
  43. package/dist/bot.js +736 -184
  44. package/dist/bot.js.map +1 -1
  45. package/dist/cli/claudeProfiles.d.ts +1 -1
  46. package/dist/cli/claudeProfiles.js +1 -1
  47. package/dist/cli/hot.d.ts +7 -1
  48. package/dist/cli/hot.d.ts.map +1 -1
  49. package/dist/cli/hot.js +29 -3
  50. package/dist/cli/hot.js.map +1 -1
  51. package/dist/configDirFile.d.ts +73 -0
  52. package/dist/configDirFile.d.ts.map +1 -0
  53. package/dist/configDirFile.js +216 -0
  54. package/dist/configDirFile.js.map +1 -0
  55. package/dist/connectors/telegram/commands/autoContinueLimits.d.ts +2 -2
  56. package/dist/connectors/telegram/commands/autoContinueLimits.d.ts.map +1 -1
  57. package/dist/connectors/telegram/commands/autoContinueLimits.js +4 -4
  58. package/dist/connectors/telegram/commands/autoContinueLimits.js.map +1 -1
  59. package/dist/connectors/telegram/commands/backendSwitch.d.ts +25 -0
  60. package/dist/connectors/telegram/commands/backendSwitch.d.ts.map +1 -0
  61. package/dist/connectors/telegram/commands/backendSwitch.js +104 -0
  62. package/dist/connectors/telegram/commands/backendSwitch.js.map +1 -0
  63. package/dist/connectors/telegram/commands/callbackMessage.d.ts +12 -0
  64. package/dist/connectors/telegram/commands/callbackMessage.d.ts.map +1 -1
  65. package/dist/connectors/telegram/commands/callbackMessage.js +24 -0
  66. package/dist/connectors/telegram/commands/callbackMessage.js.map +1 -1
  67. package/dist/connectors/telegram/commands/callbackToast.d.ts +36 -0
  68. package/dist/connectors/telegram/commands/callbackToast.d.ts.map +1 -0
  69. package/dist/connectors/telegram/commands/callbackToast.js +83 -0
  70. package/dist/connectors/telegram/commands/callbackToast.js.map +1 -0
  71. package/dist/connectors/telegram/commands/claudeAccounts.d.ts +18 -0
  72. package/dist/connectors/telegram/commands/claudeAccounts.d.ts.map +1 -1
  73. package/dist/connectors/telegram/commands/claudeAccounts.js +220 -66
  74. package/dist/connectors/telegram/commands/claudeAccounts.js.map +1 -1
  75. package/dist/connectors/telegram/commands/displayModes.d.ts.map +1 -1
  76. package/dist/connectors/telegram/commands/displayModes.js +4 -2
  77. package/dist/connectors/telegram/commands/displayModes.js.map +1 -1
  78. package/dist/connectors/telegram/commands/fallbackChain.d.ts +5 -8
  79. package/dist/connectors/telegram/commands/fallbackChain.d.ts.map +1 -1
  80. package/dist/connectors/telegram/commands/fallbackChain.js +183 -193
  81. package/dist/connectors/telegram/commands/fallbackChain.js.map +1 -1
  82. package/dist/connectors/telegram/commands/limitsBoard.d.ts +40 -0
  83. package/dist/connectors/telegram/commands/limitsBoard.d.ts.map +1 -0
  84. package/dist/connectors/telegram/commands/limitsBoard.js +281 -0
  85. package/dist/connectors/telegram/commands/limitsBoard.js.map +1 -0
  86. package/dist/connectors/telegram/commands/modelPage.d.ts +130 -0
  87. package/dist/connectors/telegram/commands/modelPage.d.ts.map +1 -0
  88. package/dist/connectors/telegram/commands/modelPage.js +120 -0
  89. package/dist/connectors/telegram/commands/modelPage.js.map +1 -0
  90. package/dist/connectors/telegram/commands/modelProviders.d.ts +9 -1
  91. package/dist/connectors/telegram/commands/modelProviders.d.ts.map +1 -1
  92. package/dist/connectors/telegram/commands/modelProviders.js +203 -111
  93. package/dist/connectors/telegram/commands/modelProviders.js.map +1 -1
  94. package/dist/connectors/telegram/commands/stt.d.ts +32 -0
  95. package/dist/connectors/telegram/commands/stt.d.ts.map +1 -0
  96. package/dist/connectors/telegram/commands/stt.js +460 -0
  97. package/dist/connectors/telegram/commands/stt.js.map +1 -0
  98. package/dist/effortLevels.d.ts +57 -7
  99. package/dist/effortLevels.d.ts.map +1 -1
  100. package/dist/effortLevels.js +65 -9
  101. package/dist/effortLevels.js.map +1 -1
  102. package/dist/i18n/de.d.ts.map +1 -1
  103. package/dist/i18n/de.js +160 -17
  104. package/dist/i18n/de.js.map +1 -1
  105. package/dist/i18n/en.d.ts.map +1 -1
  106. package/dist/i18n/en.js +160 -17
  107. package/dist/i18n/en.js.map +1 -1
  108. package/dist/i18n/es.d.ts.map +1 -1
  109. package/dist/i18n/es.js +160 -17
  110. package/dist/i18n/es.js.map +1 -1
  111. package/dist/i18n/fr.d.ts.map +1 -1
  112. package/dist/i18n/fr.js +160 -17
  113. package/dist/i18n/fr.js.map +1 -1
  114. package/dist/i18n/hi.d.ts.map +1 -1
  115. package/dist/i18n/hi.js +160 -17
  116. package/dist/i18n/hi.js.map +1 -1
  117. package/dist/i18n/ja.d.ts.map +1 -1
  118. package/dist/i18n/ja.js +160 -17
  119. package/dist/i18n/ja.js.map +1 -1
  120. package/dist/i18n/ka.d.ts.map +1 -1
  121. package/dist/i18n/ka.js +160 -17
  122. package/dist/i18n/ka.js.map +1 -1
  123. package/dist/i18n/pt.d.ts.map +1 -1
  124. package/dist/i18n/pt.js +160 -17
  125. package/dist/i18n/pt.js.map +1 -1
  126. package/dist/i18n/ru.d.ts.map +1 -1
  127. package/dist/i18n/ru.js +160 -17
  128. package/dist/i18n/ru.js.map +1 -1
  129. package/dist/i18n/uk.d.ts.map +1 -1
  130. package/dist/i18n/uk.js +160 -17
  131. package/dist/i18n/uk.js.map +1 -1
  132. package/dist/i18n/uz.d.ts.map +1 -1
  133. package/dist/i18n/uz.js +160 -17
  134. package/dist/i18n/uz.js.map +1 -1
  135. package/dist/i18n/zh.d.ts.map +1 -1
  136. package/dist/i18n/zh.js +160 -17
  137. package/dist/i18n/zh.js.map +1 -1
  138. package/dist/installManager.d.ts +20 -4
  139. package/dist/installManager.d.ts.map +1 -1
  140. package/dist/installManager.js +82 -14
  141. package/dist/installManager.js.map +1 -1
  142. package/dist/instanceSettings.d.ts +52 -0
  143. package/dist/instanceSettings.d.ts.map +1 -0
  144. package/dist/instanceSettings.js +136 -0
  145. package/dist/instanceSettings.js.map +1 -0
  146. package/dist/pinnedStatus.d.ts +7 -1
  147. package/dist/pinnedStatus.d.ts.map +1 -1
  148. package/dist/pinnedStatus.js +1 -1
  149. package/dist/pinnedStatus.js.map +1 -1
  150. package/dist/requests/requestLedger.d.ts +8 -0
  151. package/dist/requests/requestLedger.d.ts.map +1 -1
  152. package/dist/requests/requestLedger.js +19 -1
  153. package/dist/requests/requestLedger.js.map +1 -1
  154. package/dist/scheduler/mcpSurface.js +4 -4
  155. package/dist/scheduler/mcpSurface.js.map +1 -1
  156. package/dist/shutdown.d.ts +14 -0
  157. package/dist/shutdown.d.ts.map +1 -1
  158. package/dist/shutdown.js +16 -0
  159. package/dist/shutdown.js.map +1 -1
  160. package/dist/state.d.ts +54 -2
  161. package/dist/state.d.ts.map +1 -1
  162. package/dist/state.js +62 -1
  163. package/dist/state.js.map +1 -1
  164. package/dist/stt/sttAutoInstall.d.ts +39 -0
  165. package/dist/stt/sttAutoInstall.d.ts.map +1 -0
  166. package/dist/stt/sttAutoInstall.js +158 -0
  167. package/dist/stt/sttAutoInstall.js.map +1 -0
  168. package/dist/stt/sttChildEnv.d.ts +11 -0
  169. package/dist/stt/sttChildEnv.d.ts.map +1 -0
  170. package/dist/stt/sttChildEnv.js +30 -0
  171. package/dist/stt/sttChildEnv.js.map +1 -0
  172. package/dist/stt/sttConfigEdit.d.ts +71 -0
  173. package/dist/stt/sttConfigEdit.d.ts.map +1 -0
  174. package/dist/stt/sttConfigEdit.js +144 -0
  175. package/dist/stt/sttConfigEdit.js.map +1 -0
  176. package/dist/stt/sttInstall.d.ts +147 -0
  177. package/dist/stt/sttInstall.d.ts.map +1 -0
  178. package/dist/stt/sttInstall.js +491 -0
  179. package/dist/stt/sttInstall.js.map +1 -0
  180. package/dist/stt/sttInstallEntry.d.ts +2 -0
  181. package/dist/stt/sttInstallEntry.d.ts.map +1 -0
  182. package/dist/stt/sttInstallEntry.js +31 -0
  183. package/dist/stt/sttInstallEntry.js.map +1 -0
  184. package/dist/stt/sttInstallReporter.d.ts +45 -0
  185. package/dist/stt/sttInstallReporter.d.ts.map +1 -0
  186. package/dist/stt/sttInstallReporter.js +57 -0
  187. package/dist/stt/sttInstallReporter.js.map +1 -0
  188. package/dist/stt/sttRuntime.d.ts +35 -0
  189. package/dist/stt/sttRuntime.d.ts.map +1 -0
  190. package/dist/stt/sttRuntime.js +73 -0
  191. package/dist/stt/sttRuntime.js.map +1 -0
  192. package/dist/stt/sttScreenPlan.d.ts +193 -0
  193. package/dist/stt/sttScreenPlan.d.ts.map +1 -0
  194. package/dist/stt/sttScreenPlan.js +541 -0
  195. package/dist/stt/sttScreenPlan.js.map +1 -0
  196. package/dist/stt/sttSettings.d.ts +201 -0
  197. package/dist/stt/sttSettings.d.ts.map +1 -0
  198. package/dist/stt/sttSettings.js +387 -0
  199. package/dist/stt/sttSettings.js.map +1 -0
  200. package/dist/stt/sttSidecarOwner.d.ts +115 -0
  201. package/dist/stt/sttSidecarOwner.d.ts.map +1 -0
  202. package/dist/stt/sttSidecarOwner.js +475 -0
  203. package/dist/stt/sttSidecarOwner.js.map +1 -0
  204. package/dist/stt/transcribeChain.d.ts +36 -0
  205. package/dist/stt/transcribeChain.d.ts.map +1 -0
  206. package/dist/stt/transcribeChain.js +42 -0
  207. package/dist/stt/transcribeChain.js.map +1 -0
  208. package/dist/types.d.ts +77 -4
  209. package/dist/types.d.ts.map +1 -1
  210. package/dist/utils/adapterSwitchSessionIds.d.ts +32 -0
  211. package/dist/utils/adapterSwitchSessionIds.d.ts.map +1 -0
  212. package/dist/utils/adapterSwitchSessionIds.js +44 -0
  213. package/dist/utils/adapterSwitchSessionIds.js.map +1 -0
  214. package/dist/utils/backendSwitchPlan.d.ts +63 -0
  215. package/dist/utils/backendSwitchPlan.d.ts.map +1 -0
  216. package/dist/utils/backendSwitchPlan.js +73 -0
  217. package/dist/utils/backendSwitchPlan.js.map +1 -0
  218. package/dist/utils/claudeAccountPickerPlan.d.ts +12 -1
  219. package/dist/utils/claudeAccountPickerPlan.d.ts.map +1 -1
  220. package/dist/utils/claudeAccountPickerPlan.js +20 -1
  221. package/dist/utils/claudeAccountPickerPlan.js.map +1 -1
  222. package/dist/utils/claudeTranscriptContent.d.ts +24 -0
  223. package/dist/utils/claudeTranscriptContent.d.ts.map +1 -0
  224. package/dist/utils/claudeTranscriptContent.js +107 -0
  225. package/dist/utils/claudeTranscriptContent.js.map +1 -0
  226. package/dist/utils/claudeUsage.d.ts +47 -8
  227. package/dist/utils/claudeUsage.d.ts.map +1 -1
  228. package/dist/utils/claudeUsage.js +76 -14
  229. package/dist/utils/claudeUsage.js.map +1 -1
  230. package/dist/utils/claudeUsageRefresher.d.ts +8 -1
  231. package/dist/utils/claudeUsageRefresher.d.ts.map +1 -1
  232. package/dist/utils/claudeUsageRefresher.js +9 -4
  233. package/dist/utils/claudeUsageRefresher.js.map +1 -1
  234. package/dist/utils/effortPickerPlan.d.ts +54 -0
  235. package/dist/utils/effortPickerPlan.d.ts.map +1 -0
  236. package/dist/utils/effortPickerPlan.js +56 -0
  237. package/dist/utils/effortPickerPlan.js.map +1 -0
  238. package/dist/utils/ensureSessionPlan.d.ts +20 -4
  239. package/dist/utils/ensureSessionPlan.d.ts.map +1 -1
  240. package/dist/utils/ensureSessionPlan.js +17 -4
  241. package/dist/utils/ensureSessionPlan.js.map +1 -1
  242. package/dist/utils/fallbackBridge.d.ts +6 -2
  243. package/dist/utils/fallbackBridge.d.ts.map +1 -1
  244. package/dist/utils/fallbackBridge.js +10 -3
  245. package/dist/utils/fallbackBridge.js.map +1 -1
  246. package/dist/utils/fallbackChain.d.ts +36 -0
  247. package/dist/utils/fallbackChain.d.ts.map +1 -1
  248. package/dist/utils/fallbackChain.js +65 -1
  249. package/dist/utils/fallbackChain.js.map +1 -1
  250. package/dist/utils/fallbackSwitchNotice.d.ts +41 -0
  251. package/dist/utils/fallbackSwitchNotice.d.ts.map +1 -0
  252. package/dist/utils/fallbackSwitchNotice.js +75 -0
  253. package/dist/utils/fallbackSwitchNotice.js.map +1 -0
  254. package/dist/utils/fileSendService.d.ts +33 -10
  255. package/dist/utils/fileSendService.d.ts.map +1 -1
  256. package/dist/utils/fileSendService.js +78 -27
  257. package/dist/utils/fileSendService.js.map +1 -1
  258. package/dist/utils/jsonStreamHost.d.ts +39 -0
  259. package/dist/utils/jsonStreamHost.d.ts.map +1 -1
  260. package/dist/utils/jsonStreamHost.js +60 -1
  261. package/dist/utils/jsonStreamHost.js.map +1 -1
  262. package/dist/utils/limitEpisodeRecovery.d.ts +16 -2
  263. package/dist/utils/limitEpisodeRecovery.d.ts.map +1 -1
  264. package/dist/utils/limitEpisodeRecovery.js +44 -3
  265. package/dist/utils/limitEpisodeRecovery.js.map +1 -1
  266. package/dist/utils/limitSettings.d.ts +25 -0
  267. package/dist/utils/limitSettings.d.ts.map +1 -0
  268. package/dist/utils/limitSettings.js +31 -0
  269. package/dist/utils/limitSettings.js.map +1 -0
  270. package/dist/utils/limitsBoardPlan.d.ts +52 -0
  271. package/dist/utils/limitsBoardPlan.d.ts.map +1 -0
  272. package/dist/utils/limitsBoardPlan.js +93 -0
  273. package/dist/utils/limitsBoardPlan.js.map +1 -0
  274. package/dist/utils/openCodeUsage.d.ts +93 -0
  275. package/dist/utils/openCodeUsage.d.ts.map +1 -0
  276. package/dist/utils/openCodeUsage.js +248 -0
  277. package/dist/utils/openCodeUsage.js.map +1 -0
  278. package/dist/utils/operatorSwitchPlan.d.ts +61 -0
  279. package/dist/utils/operatorSwitchPlan.d.ts.map +1 -0
  280. package/dist/utils/operatorSwitchPlan.js +32 -0
  281. package/dist/utils/operatorSwitchPlan.js.map +1 -0
  282. package/dist/utils/promptDelivery.d.ts +40 -0
  283. package/dist/utils/promptDelivery.d.ts.map +1 -1
  284. package/dist/utils/promptDelivery.js +31 -0
  285. package/dist/utils/promptDelivery.js.map +1 -1
  286. package/dist/utils/recvPreviewRedaction.d.ts +14 -4
  287. package/dist/utils/recvPreviewRedaction.d.ts.map +1 -1
  288. package/dist/utils/recvPreviewRedaction.js +19 -7
  289. package/dist/utils/recvPreviewRedaction.js.map +1 -1
  290. package/dist/utils/startupReadiness.d.ts +3 -3
  291. package/dist/utils/startupReadiness.d.ts.map +1 -1
  292. package/dist/utils/startupReadiness.js +2 -2
  293. package/dist/utils/startupReadiness.js.map +1 -1
  294. package/dist/utils/threadStatusReport.d.ts +3 -1
  295. package/dist/utils/threadStatusReport.d.ts.map +1 -1
  296. package/dist/utils/threadStatusReport.js +4 -3
  297. package/dist/utils/threadStatusReport.js.map +1 -1
  298. package/dist/utils/timezone.d.ts +23 -0
  299. package/dist/utils/timezone.d.ts.map +1 -1
  300. package/dist/utils/timezone.js +38 -0
  301. package/dist/utils/timezone.js.map +1 -1
  302. package/dist/utils/transcribeAudio.d.ts +25 -12
  303. package/dist/utils/transcribeAudio.d.ts.map +1 -1
  304. package/dist/utils/transcribeAudio.js +39 -21
  305. package/dist/utils/transcribeAudio.js.map +1 -1
  306. package/dist/utils/usageRefreshBudget.d.ts +9 -0
  307. package/dist/utils/usageRefreshBudget.d.ts.map +1 -0
  308. package/dist/utils/usageRefreshBudget.js +27 -0
  309. package/dist/utils/usageRefreshBudget.js.map +1 -0
  310. package/examples/.env.example +7 -3
  311. package/examples/docker-compose.yml +9 -7
  312. package/package.json +5 -4
  313. package/stt/gigaam_sidecar.py +800 -0
@@ -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 three shapes, in order:
108
- * - relative: "resets in 2h" / "in 45m" → `now + duration`.
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 is load-bearing: the clock is then resolved IN THAT ZONE, not in the
112
- * instance/host zone. Without it a `10:50pm (UTC)` reset on a `+04:00` box
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 → `Date.parse`.
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;AAQlD;;;GAGG;AACH,eAAO,MAAM,uBAAuB,sBAAuB,CAAC;AAC5D,oFAAoF;AACpF,eAAO,MAAM,oBAAoB,GAAiC,CAAC;AAEnE;;;;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;AA2BpC;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,QAAyE,CAAC;AA2CzG;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,QAGxB,CAAC;AAEZ,0EAA0E;AAC1E,eAAO,MAAM,mBAAmB,QAA0C,CAAC;AAwB3E;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAI3D;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAiB1F;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAsB1E;AA8DD,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;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS,CAe3D;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"}
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"}
@@ -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 = 60_000;
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
- /** Matches reset phrasings like "resets in 2h", "in 45m", "in 90 min". */
59
- const relativeResetRegex = /\bin\s+(\d+)\s*(h|hours?|m|min|minutes?)\b/i;
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
- /** Matches an ISO-8601 timestamp anywhere in the text. */
77
- const isoTimestampRegex = /\b\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?\b/;
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
- String.raw `(?:hit|reached|exceeded)\s+your\s+(?:[\w-]+\s+){0,3}limit`,
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 (exhaustedQuotaCodesRe.test(text) || subscriptionLimitMessageRe.test(text)) {
208
- return { kind: 'usageLimit', resetAt: parseResetAt(text, now) };
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: parseResetAt(text, now) };
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 three shapes, in order:
225
- * - relative: "resets in 2h" / "in 45m" → `now + duration`.
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 is load-bearing: the clock is then resolved IN THAT ZONE, not in the
229
- * instance/host zone. Without it a `10:50pm (UTC)` reset on a `+04:00` box
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 → `Date.parse`.
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
- const amount = Number.parseInt(relativeMatch[1], 10);
244
- const unit = relativeMatch[2].toLowerCase();
245
- const isHours = unit.startsWith('h');
246
- return now + amount * (isHours ? msPerHour : msPerMinute);
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
- const parsed = Date.parse(isoMatch[0]);
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 meridiem = match[3]?.toLowerCase();
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 zoneMatch = trailingZoneRegex.exec(text.slice(match.index + match[0].length));
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 };