dsh-codex-subscription 2.1.9 → 2.2.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/README.en.md CHANGED
@@ -39,6 +39,7 @@ See below for detailed installation steps, terminal commands, updates, and remov
39
39
  | **Subscription models** | Sign in to ChatGPT and use Codex without an OpenAI API key or Codex CLI |
40
40
  | **Recoverable and diagnosable** | Sign-in state reconciles automatically; failed reads can be retried in place, while timeouts and stale account responses cannot overwrite current state; Settings can create a support report without credentials or account identifiers |
41
41
  | **Visible quota** | Keep backend-provided standard Codex, Spark, and other limits separate, with reset times |
42
+ | **Optional quota recovery** | Opt in to keep a turn open until a confirmed short Codex quota resets; the turn shows the wait and remains cancellable, while switching accounts retries immediately |
42
43
  | **Composer quota** | Choose a compact percentage, progress bar, Beta runway forecast, or no inline display |
43
44
  | **Safe quota reset** | See each reset credit separately and deliberately try one with a cooldown and acknowledgement |
44
45
  | **Subscription search** | Explicitly route search globally through DSH default search or the signed-in Codex subscription |
package/README.md CHANGED
@@ -40,6 +40,7 @@
40
40
  | **订阅模型直连** | 登录 ChatGPT 后直接使用 Codex,不需要 OpenAI API Key 或 Codex CLI |
41
41
  | **可恢复、可诊断** | 登录状态会自动对账;读取失败时可在原处重试,超时和旧账号响应不会覆盖当前状态;设置页可生成不含凭据和账号标识的支持报告 |
42
42
  | **额度可见** | 普通 Codex、Spark 等服务端实际返回的额度分开显示,并显示重置时间 |
43
+ | **可选额度恢复** | 可选择让对话等待已确认的短时 Codex 额度重置;对话会显示等待原因、仍可停止,等待期间切换账号会立即重试 |
43
44
  | **输入框额度** | 可选择紧凑百分比、进度条、Beta 续航预测或关闭显示 |
44
45
  | **安全额度重置** | 每张重置卡单独显示,并通过冷静期和知情确认主动尝试重置 |
45
46
  | **订阅搜索** | 可将全部模型的搜索明确路由到 DSH 默认搜索或已登录的 Codex 订阅 |
package/lib/client.js CHANGED
@@ -4878,6 +4878,8 @@ window.__ModuleLoader__.load({
4878
4878
  quickQuotaBar: "进度条",
4879
4879
  quickQuotaForecast: "续航预测",
4880
4880
  quickQuotaBeta: "Beta",
4881
+ autoQuotaRetry: "额度重置后自动重试",
4882
+ autoQuotaRetryHint: "选择开启后,将等待已确认的短时额度重置;对话会显示等待原因且可停止,等待期间切换账号会立即重试。",
4881
4883
  quickQuotaForecastHint: "按最近两小时的消耗估算;消耗不足或速度突变时重新校准,详情显示估计范围。仅供参考,历史保存在本机。",
4882
4884
  contextTitle: "上下文窗口",
4883
4885
  contextStandard: "标准",
@@ -5332,6 +5334,8 @@ window.__ModuleLoader__.load({
5332
5334
  quickQuotaBar: "Progress bar",
5333
5335
  quickQuotaForecast: "Runway",
5334
5336
  quickQuotaBeta: "Beta",
5337
+ autoQuotaRetry: "Retry after quota reset",
5338
+ autoQuotaRetryHint: "Opt in to wait for confirmed short quota resets. The turn shows why it is waiting and can be stopped; switching accounts retries immediately.",
5335
5339
  quickQuotaForecastHint: "Estimates recent two-hour consumption; recalibrates with insufficient data or changing pace. Details show a range. Indicative only; history stays local.",
5336
5340
  contextTitle: "Context window",
5337
5341
  contextStandard: "Standard",
@@ -5470,6 +5474,12 @@ window.__ModuleLoader__.load({
5470
5474
  .codexSubscriptionPreferenceCopy{display:flex;min-width:0;flex-direction:column;gap:2px}
5471
5475
  .codexSubscriptionPreferenceLabel{display:flex;align-items:center;gap:6px}
5472
5476
  .codexSubscriptionPreferenceHint{max-width:300px;font-size:11px;line-height:17px;color:var(--dsw-alias-label-tertiary)}
5477
+ .codexSubscriptionSwitch{position:relative;flex:none;width:36px;height:20px;padding:2px;border:0;border-radius:999px;background:var(--dsw-alias-bg-module-platform);cursor:pointer;transition:background 120ms var(--ds-ease-in-out)}
5478
+ .codexSubscriptionSwitch[aria-checked=true]{background:var(--dsw-alias-label-primary)}
5479
+ .codexSubscriptionSwitch:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:2px}
5480
+ .codexSubscriptionSwitch:disabled{cursor:not-allowed;opacity:.5}
5481
+ .codexSubscriptionSwitchKnob{display:block;width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-bg-layer-1);box-shadow:0 1px 2px var(--dsw-alias-border-l3);transition:transform 120ms var(--ds-ease-in-out)}
5482
+ .codexSubscriptionSwitch[aria-checked=true] .codexSubscriptionSwitchKnob{transform:translateX(16px)}
5473
5483
  .codexSubscriptionQuotaModes{display:flex;align-items:center;gap:3px;padding:2px;border-radius:9px;background:var(--dsw-alias-bg-module-platform)}
5474
5484
  .codexSubscriptionQuotaMode{position:relative;display:flex;align-items:center;justify-content:center;min-height:26px;padding:0 9px;border-radius:7px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;cursor:pointer;white-space:nowrap}
5475
5485
  .codexSubscriptionQuotaMode small{margin-left:3px;font-size:9px;line-height:1;color:var(--dsw-alias-label-tertiary)}
@@ -6687,6 +6697,7 @@ window.__ModuleLoader__.load({
6687
6697
  limitId: limit.id
6688
6698
  }))).filter((window) => Number.isFinite(window.remainingPercent) && window.remainingPercent >= 0 && window.remainingPercent <= (mode === "custom" ? readCapabilitySettings(thresholds)[window.windowSeconds > 0 && window.windowSeconds <= 6 * 3600 ? "quotaShortThreshold" : "quotaLongThreshold"] : mode === "early" && window.windowSeconds > 0 && window.windowSeconds <= 6 * 3600 ? 50 : 20) && (!Number.isFinite(window.resetsAt) || window.resetsAt * 1e3 > now)).sort((a, b) => a.remainingPercent - b.remainingPercent)[0];
6689
6699
  }
6700
+ const AUTO_QUOTA_RETRY_FIELD = "autoQuotaRetry";
6690
6701
  const QUICK_QUOTA_MODE_FIELD = "quickQuotaMode";
6691
6702
  const LEGACY_QUICK_QUOTA_FIELD = "quickQuotaVisible";
6692
6703
  const QUICK_QUOTA_MODE_PERCENT = "percent";
@@ -6734,6 +6745,7 @@ window.__ModuleLoader__.load({
6734
6745
  "gpt-5.6": 272e3,
6735
6746
  "gpt-6-astra": 272e3
6736
6747
  });
6748
+ const normalizeAutoQuotaRetry = (value) => typeof value === "boolean" ? value : false;
6737
6749
  const normalizeSearchProvider = (value) => [
6738
6750
  "auto",
6739
6751
  "dsh",
@@ -6805,6 +6817,7 @@ window.__ModuleLoader__.load({
6805
6817
  return Object.freeze({
6806
6818
  status: current.status,
6807
6819
  ...capabilities,
6820
+ autoQuotaRetry: normalizeAutoQuotaRetry(value?.[AUTO_QUOTA_RETRY_FIELD]),
6808
6821
  connectionMode: value?.connectionMode === "websocket" ? "websocket" : "sse",
6809
6822
  compactionMode: value?.compactionMode === "cloud" ? "cloud" : "dsh",
6810
6823
  subagentBackend: value?.subagentBackend === "codex" ? "codex" : "dsh",
@@ -6855,6 +6868,7 @@ window.__ModuleLoader__.load({
6855
6868
  fallback = {
6856
6869
  status: "ready",
6857
6870
  value: {
6871
+ [AUTO_QUOTA_RETRY_FIELD]: normalizeAutoQuotaRetry(value?.[AUTO_QUOTA_RETRY_FIELD]),
6858
6872
  connectionMode: value?.connectionMode === "websocket" ? "websocket" : "sse",
6859
6873
  compactionMode: value?.compactionMode === "cloud" ? "cloud" : "dsh",
6860
6874
  subagentBackend: value?.subagentBackend === "codex" ? "codex" : "dsh",
@@ -7276,7 +7290,7 @@ window.__ModuleLoader__.load({
7276
7290
  }
7277
7291
  //#endregion
7278
7292
  //#region src/version.js
7279
- const PACKAGE_VERSION = "2.1.9";
7293
+ const PACKAGE_VERSION = "2.2.0";
7280
7294
  //#endregion
7281
7295
  //#region src/client-recovery.js
7282
7296
  async function recoveryCall(rpc, endpoint, payload = {}, timeoutMs = 1e4) {
@@ -8353,6 +8367,34 @@ window.__ModuleLoader__.load({
8353
8367
  })]
8354
8368
  });
8355
8369
  }
8370
+ function AutoQuotaRetryPreference({ preference, t }) {
8371
+ const snapshot = usePreferenceSnapshot(preference);
8372
+ const writable = snapshot.status === "ready" && snapshot.writable === true;
8373
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8374
+ className: "codexSubscriptionPreference",
8375
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8376
+ className: "codexSubscriptionPreferenceCopy",
8377
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8378
+ className: "codexSubscriptionPreferenceLabel",
8379
+ children: t("autoQuotaRetry")
8380
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8381
+ className: "codexSubscriptionPreferenceHint",
8382
+ children: t("autoQuotaRetryHint")
8383
+ })]
8384
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
8385
+ className: "codexSubscriptionSwitch",
8386
+ type: "button",
8387
+ role: "switch",
8388
+ "aria-label": t("autoQuotaRetry"),
8389
+ "aria-checked": snapshot.autoQuotaRetry,
8390
+ disabled: !writable,
8391
+ onClick: () => {
8392
+ preference.set({ [AUTO_QUOTA_RETRY_FIELD]: !snapshot.autoQuotaRetry });
8393
+ },
8394
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "codexSubscriptionSwitchKnob" })
8395
+ })]
8396
+ });
8397
+ }
8356
8398
  function SearchProviderPreference({ preference, t }) {
8357
8399
  const snapshot = usePreferenceSnapshot(preference);
8358
8400
  const writable = snapshot.status === "ready" && snapshot.writable === true;
@@ -8700,14 +8742,22 @@ window.__ModuleLoader__.load({
8700
8742
  t
8701
8743
  })]
8702
8744
  })
8703
- ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickQuotaPreference, {
8704
- preference,
8705
- t
8706
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilityPreferences, {
8707
- preference,
8708
- t,
8709
- section: "quota"
8710
- })] }), snapshot.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8745
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
8746
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuickQuotaPreference, {
8747
+ preference,
8748
+ t
8749
+ }),
8750
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilityPreferences, {
8751
+ preference,
8752
+ t,
8753
+ section: "quota"
8754
+ }),
8755
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "codexSubscriptionDivider" }),
8756
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AutoQuotaRetryPreference, {
8757
+ preference,
8758
+ t
8759
+ })
8760
+ ] }), snapshot.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8711
8761
  className: "codexSubscriptionRecover",
8712
8762
  role: "alert",
8713
8763
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
package/lib/index.js CHANGED
@@ -193,6 +193,7 @@ function capabilityPatch(payload) {
193
193
  //#endregion
194
194
  //#region src/settings-contract.js
195
195
  const SETTINGS_NAMESPACE = "codex-subscription";
196
+ const AUTO_QUOTA_RETRY_FIELD = "autoQuotaRetry";
196
197
  const QUICK_QUOTA_MODE_FIELD = "quickQuotaMode";
197
198
  const LEGACY_QUICK_QUOTA_FIELD = "quickQuotaVisible";
198
199
  const QUICK_QUOTA_MODE_PERCENT = "percent";
@@ -240,6 +241,7 @@ const CUSTOM_CONTEXT_MODEL_DEFAULTS = Object.freeze({
240
241
  "gpt-5.6": 272e3,
241
242
  "gpt-6-astra": 272e3
242
243
  });
244
+ const normalizeAutoQuotaRetry = (value) => typeof value === "boolean" ? value : false;
243
245
  const normalizeOutputVerbosity = (value) => [
244
246
  "default",
245
247
  "low",
@@ -2829,7 +2831,7 @@ function openaiCodexSubscriptionProvider({ resolveSpeedMode = () => void 0, reso
2829
2831
  Object.freeze(["0.82.1", "0.85.1"]);
2830
2832
  //#endregion
2831
2833
  //#region src/version.js
2832
- const PACKAGE_VERSION = "2.1.9";
2834
+ const PACKAGE_VERSION = "2.2.0";
2833
2835
  const USER_AGENT = `dsh-codex-subscription/${PACKAGE_VERSION}`;
2834
2836
  //#endregion
2835
2837
  //#region src/model-catalog.js
@@ -3972,6 +3974,7 @@ async function createSubscriptionDiagnostics({ auth, preferences, login = { phas
3972
3974
  ...gaps.length ? { unsupported: gaps } : {}
3973
3975
  } } : {},
3974
3976
  configuration: {
3977
+ ...typeof preference.autoQuotaRetry === "boolean" ? { autoQuotaRetry: preference.autoQuotaRetry } : {},
3975
3978
  contextMode: preference.contextMode,
3976
3979
  quickQuotaMode: preference.quickQuotaMode,
3977
3980
  ...typeof preference.outputVerbosity === "string" ? { outputVerbosity: preference.outputVerbosity } : {},
@@ -4209,6 +4212,187 @@ function createCodexUsageReader(options) {
4209
4212
  });
4210
4213
  }
4211
4214
  //#endregion
4215
+ //#region src/quota-retry.js
4216
+ const DEFAULT_MAX_WAIT_MS = 360 * 60 * 1e3;
4217
+ const DEFAULT_RESET_MARGIN_MS = 1e4;
4218
+ const EXHAUSTED_PERCENT = 99.9;
4219
+ const CODEX_USAGE_LIMIT_MESSAGE = /^You have hit your ChatGPT usage limit(?: \([a-z0-9][a-z0-9 ._-]{0,39} plan\))?\.(?: Try again in ~\d{1,6} min\.)?$/u;
4220
+ const isCodexRateLimit = (failure) => failure?.code === "RATE_LIMIT" || failure?.code === "PI_AI_ERROR" && typeof failure.message === "string" && CODEX_USAGE_LIMIT_MESSAGE.test(failure.message);
4221
+ function cancellableDelay(delayMs, signal) {
4222
+ if (signal?.aborted) return Promise.resolve(false);
4223
+ return new Promise((resolve) => {
4224
+ const timer = setTimeout(() => {
4225
+ signal?.removeEventListener("abort", onAbort);
4226
+ resolve(true);
4227
+ }, delayMs);
4228
+ function onAbort() {
4229
+ clearTimeout(timer);
4230
+ resolve(false);
4231
+ }
4232
+ signal?.addEventListener("abort", onAbort, { once: true });
4233
+ });
4234
+ }
4235
+ function createWakeSignal() {
4236
+ let revision = 0;
4237
+ let reason;
4238
+ const waiters = /* @__PURE__ */ new Set();
4239
+ const notify = (nextReason) => {
4240
+ revision += 1;
4241
+ reason = nextReason;
4242
+ for (const waiter of [...waiters]) waiter(nextReason);
4243
+ };
4244
+ const wait = (since, signal) => {
4245
+ if (revision !== since) return Promise.resolve(reason);
4246
+ if (signal?.aborted) return Promise.resolve("cancelled");
4247
+ return new Promise((resolve) => {
4248
+ const settle = (value) => {
4249
+ waiters.delete(settle);
4250
+ signal?.removeEventListener("abort", onAbort);
4251
+ resolve(value);
4252
+ };
4253
+ const onAbort = () => settle("cancelled");
4254
+ waiters.add(settle);
4255
+ signal?.addEventListener("abort", onAbort, { once: true });
4256
+ if (revision !== since) settle(reason);
4257
+ });
4258
+ };
4259
+ return {
4260
+ revision: () => revision,
4261
+ notify,
4262
+ wait
4263
+ };
4264
+ }
4265
+ function shortResetDelay(usage, nowMs, maxWaitMs, resetMarginMs) {
4266
+ let latestDelayMs;
4267
+ for (const limit of usage?.rateLimits ?? []) {
4268
+ if (limit?.id !== "codex") continue;
4269
+ for (const window of limit?.windows ?? []) {
4270
+ if (!Number.isFinite(window?.usedPercent) || window.usedPercent < EXHAUSTED_PERCENT) continue;
4271
+ if (!Number.isFinite(window?.windowSeconds) || window.windowSeconds * 1e3 > maxWaitMs) return void 0;
4272
+ if (!Number.isFinite(window?.resetsAt)) return void 0;
4273
+ const delayMs = window.resetsAt * 1e3 - nowMs;
4274
+ if (delayMs <= 0 || delayMs + resetMarginMs > maxWaitMs) return void 0;
4275
+ latestDelayMs = Math.max(latestDelayMs ?? 0, delayMs);
4276
+ }
4277
+ }
4278
+ return latestDelayMs;
4279
+ }
4280
+ function publishScheduledRetry({ agent, turn, step, provider, failure }, delayMs, resetAtMs) {
4281
+ if (typeof agent?.session?.append !== "function" || !Number.isSafeInteger(turn) || !Number.isSafeInteger(step)) return void 0;
4282
+ const retryId = `codex-quota-${randomUUID()}`;
4283
+ const message = `Codex usage limit exhausted. Waiting for quota reset at ${new Date(resetAtMs).toISOString()}; retrying shortly afterward.`;
4284
+ const visibleFailure = {
4285
+ code: failure.code,
4286
+ message,
4287
+ ...Number.isInteger(failure.status) && failure.status >= 100 && failure.status <= 599 ? { status: failure.status } : {},
4288
+ ...typeof failure.requestId === "string" && failure.requestId.length > 0 ? { requestId: failure.requestId } : {}
4289
+ };
4290
+ try {
4291
+ agent.session.append("llm/retry", {
4292
+ retryId,
4293
+ turn,
4294
+ step,
4295
+ provider,
4296
+ mode: "normal",
4297
+ policyKey: retryId,
4298
+ retry: 1,
4299
+ maxRetries: 1,
4300
+ delayMs,
4301
+ failure: visibleFailure
4302
+ });
4303
+ return {
4304
+ retryId,
4305
+ turn,
4306
+ step,
4307
+ retry: 1
4308
+ };
4309
+ } catch {
4310
+ return;
4311
+ }
4312
+ }
4313
+ function publishRetryStarted(agent, scheduled) {
4314
+ try {
4315
+ agent.session.append("llm/retry-started", scheduled);
4316
+ return true;
4317
+ } catch {
4318
+ return false;
4319
+ }
4320
+ }
4321
+ async function waitForResetOrWake(delayMs, signal, wait, wakeSignal, revision) {
4322
+ const controller = new AbortController();
4323
+ const fusedSignal = signal === void 0 ? controller.signal : AbortSignal.any([signal, controller.signal]);
4324
+ const delayed = Promise.resolve(wait(delayMs, fusedSignal)).then((completed) => completed ? "reset" : "cancelled");
4325
+ const result = await Promise.race([delayed, wakeSignal.wait(revision, fusedSignal)]);
4326
+ controller.abort();
4327
+ return result;
4328
+ }
4329
+ /**
4330
+ * Recover a Codex subscription RATE_LIMIT only when the backend confirms an
4331
+ * exhausted short quota window whose reset is close enough to wait out.
4332
+ *
4333
+ * The listener owns the authoritative wait from `/wham/usage` and publishes
4334
+ * the host's standard model-retry surface event so the turn shows the reason,
4335
+ * countdown, and normal turn cancellation affordance while it is parked.
4336
+ */
4337
+ function createCodexQuotaRetryHandler({ usageReader, provider = "openai-codex", enabled = () => true, now = Date.now, wait = cancellableDelay, maxWaitMs = DEFAULT_MAX_WAIT_MS, resetMarginMs = DEFAULT_RESET_MARGIN_MS } = {}) {
4338
+ if (usageReader === void 0 || typeof usageReader.read !== "function") throw new TypeError("quota retry requires a usage reader");
4339
+ if (typeof enabled !== "function" || typeof now !== "function" || typeof wait !== "function") throw new TypeError("quota retry requires enablement, clock, and wait functions");
4340
+ if (!Number.isFinite(maxWaitMs) || maxWaitMs <= 0 || !Number.isFinite(resetMarginMs) || resetMarginMs < 0) throw new TypeError("quota retry requires a positive wait cap and non-negative reset margin");
4341
+ const wakeSignal = createWakeSignal();
4342
+ const clearCache = async () => {
4343
+ try {
4344
+ if (typeof usageReader.clearCache === "function") await usageReader.clearCache();
4345
+ else await usageReader.clear?.();
4346
+ } catch {}
4347
+ };
4348
+ const handler = async ({ agent, turn, step, provider: requestProvider, failure, signal }, next) => {
4349
+ if (requestProvider !== provider || !isCodexRateLimit(failure) || !enabled()) return next();
4350
+ if (signal?.aborted) return void 0;
4351
+ let wakeRevision = wakeSignal.revision();
4352
+ let usage;
4353
+ try {
4354
+ usage = await usageReader.read({
4355
+ force: true,
4356
+ signal
4357
+ });
4358
+ } catch {
4359
+ if (signal?.aborted) return void 0;
4360
+ return next();
4361
+ }
4362
+ if (signal?.aborted) return void 0;
4363
+ if (!enabled()) return next();
4364
+ const observedAtMs = now();
4365
+ let resetDelayMs = shortResetDelay(usage, observedAtMs, maxWaitMs, resetMarginMs);
4366
+ if (resetDelayMs === void 0) return next();
4367
+ const scheduled = publishScheduledRetry({
4368
+ agent,
4369
+ turn,
4370
+ step,
4371
+ provider: requestProvider,
4372
+ failure
4373
+ }, resetDelayMs + resetMarginMs, observedAtMs + resetDelayMs);
4374
+ if (scheduled === void 0) return next();
4375
+ while (true) {
4376
+ const reason = await waitForResetOrWake(resetDelayMs + resetMarginMs, signal, wait, wakeSignal, wakeRevision);
4377
+ if (reason === "cancelled" || signal?.aborted) return void 0;
4378
+ if (!enabled()) return next();
4379
+ if (reason !== "configuration") break;
4380
+ wakeRevision = wakeSignal.revision();
4381
+ resetDelayMs = shortResetDelay(usage, now(), maxWaitMs, resetMarginMs);
4382
+ if (resetDelayMs === void 0) return next();
4383
+ }
4384
+ await clearCache();
4385
+ if (signal?.aborted) return void 0;
4386
+ if (!publishRetryStarted(agent, scheduled)) return next();
4387
+ return { kind: "retry" };
4388
+ };
4389
+ Object.defineProperties(handler, {
4390
+ notifyAccountChanged: { value: () => wakeSignal.notify("account") },
4391
+ notifyConfigurationChanged: { value: () => wakeSignal.notify("configuration") }
4392
+ });
4393
+ return handler;
4394
+ }
4395
+ //#endregion
4212
4396
  //#region src/quota-rate-interval.js
4213
4397
  function quotaRateInterval(samples, { quantum = 1, rounding = "unknown" } = {}) {
4214
4398
  if (!(quantum > 0) || !Number.isFinite(quantum)) throw new RangeError("Invalid quantum");
@@ -4879,7 +5063,7 @@ const publicError = (code, message) => ({
4879
5063
  details: { issues: [] }
4880
5064
  }
4881
5065
  });
4882
- function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, runtimeManagement, diagnosticsReader, modelCatalog, originalImages, resolveInheritedOriginal, closeConnections }) {
5066
+ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditService, preferences, runtimeManagement, diagnosticsReader, modelCatalog, originalImages, resolveInheritedOriginal, closeConnections, onAccountChanged }) {
4883
5067
  return async (endpoint, payload, signal) => {
4884
5068
  if ([
4885
5069
  "runtime/status",
@@ -4950,6 +5134,10 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
4950
5134
  signal.throwIfAborted();
4951
5135
  if (endpoint === "preferences/update") {
4952
5136
  const patch = capabilityPatch(payload);
5137
+ if (Object.hasOwn(payload ?? {}, "autoQuotaRetry")) {
5138
+ if (typeof payload["autoQuotaRetry"] !== "boolean") return publicError("internal", "Invalid automatic quota retry preference");
5139
+ patch[AUTO_QUOTA_RETRY_FIELD] = payload[AUTO_QUOTA_RETRY_FIELD];
5140
+ }
4953
5141
  for (const [field, rule] of Object.entries(PREFERENCE_FIELDS)) {
4954
5142
  if (!Object.hasOwn(payload ?? {}, field)) continue;
4955
5143
  if (!rule.choices.includes(payload[field])) return publicError("internal", rule.error);
@@ -5032,10 +5220,12 @@ function createSubscriptionRpcHandler({ authHandler, usageReader, resetCreditSer
5032
5220
  await usageReader.clear();
5033
5221
  resetCreditService.clear();
5034
5222
  modelCatalog?.clear();
5223
+ onAccountChanged?.();
5035
5224
  } else if (result.ok === true && (endpoint === "account/select" || endpoint === "account/remove" || endpoint === "login/status" && result.value?.authenticated === true)) {
5036
5225
  usageReader.clearCache();
5037
5226
  resetCreditService.clear();
5038
5227
  modelCatalog?.clear();
5228
+ onAccountChanged?.();
5039
5229
  modelCatalog?.refresh({ signal: void 0 }).catch(() => {});
5040
5230
  } else if (result.ok === true && (endpoint === "status" || result.value?.authenticated === true)) modelCatalog?.refresh({ signal: void 0 }).catch(() => {});
5041
5231
  return result;
@@ -5088,6 +5278,7 @@ function createSearchProviderSwitcher(loader) {
5088
5278
  });
5089
5279
  }
5090
5280
  const settingsFields = {
5281
+ [AUTO_QUOTA_RETRY_FIELD]: z.boolean().default(false),
5091
5282
  ...Object.fromEntries(Object.entries(PREFERENCE_FIELDS).map(([field, rule]) => [field, rule.default === void 0 ? z.union(rule.choices) : z.union(rule.choices).default(rule.default)])),
5092
5283
  imageModel: z.union(Object.keys(IMAGE_MODELS)).default(DEFAULT_IMAGE_MODEL),
5093
5284
  imageQuality: z.union([
@@ -5170,6 +5361,7 @@ function apply(ctx, config = {}) {
5170
5361
  });
5171
5362
  const preferences = {
5172
5363
  status: () => ({
5364
+ [AUTO_QUOTA_RETRY_FIELD]: normalizeAutoQuotaRetry(settings.get()[AUTO_QUOTA_RETRY_FIELD]),
5173
5365
  compactionMode: settings.get().compactionMode ?? "dsh",
5174
5366
  connectionMode: settings.get().connectionMode ?? "sse",
5175
5367
  subagentBackend: settings.get().subagentBackend ?? "dsh",
@@ -5380,6 +5572,30 @@ function apply(ctx, config = {}) {
5380
5572
  usageReader.clearCache();
5381
5573
  };
5382
5574
  }, "codex-subscription: quota forecast warm-up");
5575
+ const quotaRetryLifetime = new AbortController();
5576
+ const activeQuotaRetries = /* @__PURE__ */ new Set();
5577
+ const quotaRetryHandler = createCodexQuotaRetryHandler({
5578
+ usageReader,
5579
+ enabled: () => normalizeAutoQuotaRetry(settings.get()[AUTO_QUOTA_RETRY_FIELD])
5580
+ });
5581
+ const disposeQuotaRetry = ctx.on("agent/request-error", (payload, next) => {
5582
+ if (quotaRetryLifetime.signal.aborted) return Promise.resolve(void 0);
5583
+ const signal = payload.signal === void 0 ? quotaRetryLifetime.signal : AbortSignal.any([payload.signal, quotaRetryLifetime.signal]);
5584
+ const tracked = quotaRetryHandler({
5585
+ ...payload,
5586
+ signal
5587
+ }, next).finally(() => activeQuotaRetries.delete(tracked));
5588
+ activeQuotaRetries.add(tracked);
5589
+ return tracked;
5590
+ });
5591
+ ctx.effect(() => async () => {
5592
+ disposeQuotaRetry();
5593
+ quotaRetryLifetime.abort(/* @__PURE__ */ new Error("codex-subscription quota retry disposed"));
5594
+ await Promise.allSettled([...activeQuotaRetries]);
5595
+ }, "codex-subscription: abort and drain quota recovery");
5596
+ ctx.effect(() => settings.watch((value, previous) => {
5597
+ if (normalizeAutoQuotaRetry(value["autoQuotaRetry"]) !== normalizeAutoQuotaRetry(previous?.["autoQuotaRetry"])) quotaRetryHandler.notifyConfigurationChanged();
5598
+ }), "codex-subscription: update quota recovery preference");
5383
5599
  const resetCreditService = createCodexResetCreditService({
5384
5600
  getAuth: resolveAuth,
5385
5601
  readCredential: (options) => store.read(PROVIDER, options),
@@ -5412,6 +5628,7 @@ function apply(ctx, config = {}) {
5412
5628
  resetCreditService,
5413
5629
  preferences,
5414
5630
  runtimeManagement,
5631
+ onAccountChanged: quotaRetryHandler.notifyAccountChanged,
5415
5632
  diagnosticsReader: () => createSubscriptionDiagnostics({
5416
5633
  auth,
5417
5634
  preferences,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-codex-subscription",
3
- "version": "2.1.9",
3
+ "version": "2.2.0",
4
4
  "description": "Use ChatGPT and Codex subscriptions in DeepSeek Harness with OAuth, quota, safe resets, web search, images, and Fast mode",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",