Skip to content

feat(quota): detect usage-window resets and notify on them (rebase of #2783) - #3592

Merged
lidge-jun merged 5 commits into
devfrom
codex/260905-quota-reset-detection
Sep 5, 2026
Merged

feat(quota): detect usage-window resets and notify on them (rebase of #2783)#3592
lidge-jun merged 5 commits into
devfrom
codex/260905-quota-reset-detection

Conversation

@lidge-jun

Copy link
Copy Markdown
Owner

Summary

Usage-window reset detection for account quotas: OpenCodex notices when a provider's 5-hour / weekly / monthly window rolls over, records the observation, and can notify through a configured webhook. Rebase of #2783 (maintainer-authored, 23 commits) onto the merged Antigravity/Ollama quota layer, with the three blockers from the last review closed as six bounded fixes:

  • B1 webhook URL must be https:// (http:// is rejected before any send).
  • B2 webhook sends use redirect: "manual"; a 3xx is a blocked-destination result, never a followed hop (no SSRF via redirect).
  • B3 MIN_INTERVAL_MS raised to 600 000 (the docstring's "above the 10-minute per-account TTL" is now true) and MIN_POLL_SECONDS raised to 600 with it so the resolver cannot accept a cadence the poller then silently overrides.
  • B4 the configured cadence actually reaches the poller: syncQuotaResetPollerCadence() resolves it through a dynamic import() from the lifecycle so tests/usage/quota-reset-core-boundary.test.ts's zero-static-import guard on the poller stays green (the plan's direct call would have pulled the config barrel into every server start).
  • B5 overlapping poll ticks are fenced (one in flight at a time).
  • B6 a "seen" claim is durable — a claim pruned immediately is not treated as delivered.

Seven new test basenames are registered in scripts/test-layout/layout.json and tests/fixtures/test-layout-expected.json under tests/usage/. docs-site server.md corrected (600-second floor, https requirement, redirect refusal). The src/providers/quota.ts merge against layer 1 keeps layer 1's pinned-transport summary probe intact (its 121/0 guard still passes here). Supersedes #2783.

Security-boundary note (MAINTAINERS.md): B1/B2 touch outbound webhook delivery — a new egress path carrying no credentials (payload is quota observations only). https-only plus manual redirect handling is the fail-closed shape; nothing is logged beyond the delivery result. Recorded here for explicit review.

Stack (wp4 quota stack; merge bottom-up):

# Layer Branch Base
2 #2783 quota reset detection ← you are here codex/260905-quota-reset-detection codex/260905-antigravity-ollama-quota (#3587)
1 #3447 Antigravity weekly + Ollama Cloud quota codex/260905-antigravity-ollama-quota dev

Depends on #3587. Review this PR's diff only. Unit: devlog/_plan/260905_open_work_closeout/ (040, 041, 042).

Verification

  • bun run typecheck — exit 0. bun run privacy:scan — passed.
  • RED/GREEN per fix (fix reverted → applied): layout unresolvedNew 7 → 0 (15/2 → 17/0); B1 expect(result.ok) on http:// false→ok (10/0); B2 302 http-errorblocked-destination; B3 60 000 → 600 000; B4 quotaResetPollerIntervalForTests missing → 1 800 000 passes / 60 000 clamps to 600 000; B5 overlap 2 → 1 (15/0); B6 pruned claim true → false (16/0).
  • Focused (all paths ls-checked): quota-reset detector 28/0, account-key 3/0, core-boundary 14/0, notify-config 10/0, seen-store 16/0, observation 15/0, tests/lab/core-lab-boundary.test.ts 17/0, tests/providers/provider-quota.test.ts 121/0, tests/ci-workflows/repo-hygiene.test.ts 14/0, layout guard 17/0. quota-reset-notify and server-background-lifecycle bind ports and are hosted-CI-only locally (EADDRINUSE, identical before and after the change).
  • Exact-head hosted CI is the merge gate (no repository-wide local suite by maintainer instruction).

Checklist

hualiny and others added 4 commits September 5, 2026 11:43
Carries #3447 by @hualiny (Antigravity weekly windows via
retrieveUserQuotaSummary, plus Ollama Cloud quota from /api/usage),
rebased rename-aware onto tests/providers/.

The provider-level probe added there sent the stored account bearer to an
operator-configured baseUrl with default redirect following, while
fetchAntigravityUsageQuota in the same file already pins the identical
request. Route it through providerOutboundPost against
ANTIGRAVITY_ACCOUNT_QUOTA_BASE with the providerRedirectError check so a
configured baseUrl stays a routing choice for model requests, not a second
destination for Google's accounting. The fetchAvailableModels fallback is
unchanged from dev.

Co-authored-by: hualiny <82697947+hualiny@users.noreply.github.com>
…ogle's host

Co-authored-by: hualiny <82697947+hualiny@users.noreply.github.com>
…vider quota test

After the summary probe moved off globalThis.fetch, this test made a real request to Google (sandbox DNS failure masked it as a fallthrough). Inject the seam with a 404 so the fetchAvailableModels fallback is what the test exercises, as before.

Co-authored-by: hualiny <82697947+hualiny@users.noreply.github.com>
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 5, 2026 03:09
@lidge-jun lidge-jun added the enhancement New feature or request label Sep 5, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-05T03:15:22.569732Z adb0dc5 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • ^dev$
  • ^preview$

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Team

Run ID: 875b8e3a-0d0f-4849-85c2-0fd50c47ea5c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the intake: hygiene-blocked Deterministic PR hygiene checks failed label Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

⚠️ Deterministic hygiene checks failed.

  • missing_coauthor_credit — This pull request says it reimplements, supersedes, carries, or rebases another author's pull request, but no Co-authored-by trailer names that author. Prose in a commit body is not read by anything; the trailer is what GitHub counts. Add it to the description or a commit, or obtain attribution-approved. Paths: #2747, #2766.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 61 / 80

설명

이 PR은 #2783을 메인테이너가 스택 레이어 1(#3587 / codex/260905-antigravity-ollama-quota) 위로 다시 올린 리베이스입니다. 하는 일을 쉽게 말하면 이렇습니다. 계정 쿼터의 5시간·주간·월간 창이 리셋됐는지 감지하고, 같은 사건을 재시작 후에도 한 번만 기억한 뒤, 기본값 OFF인 웹훅/로컬 명령으로 알려 줍니다. 라우팅을 바꾸지 않습니다. 감지와 알림만 합니다. 원본 #2783은 아직 OPEN·CONFLICTING이고, 이 브랜치가 그걸 대체합니다.

지금 dev HEAD는 3c920af5f(package 2.43.0)입니다. 이 PR의 베이스는 dev가 아니라 #3587 브랜치입니다. 그래서 #3587이 먼저 dev에 들어가야 이 레이어를 올립니다. wp4 쿼터 스택 문서(devlog/_plan/260905_open_work_closeout/040_wp4_stack_d_usage_quota.md)도 같은 순서입니다. 레이어 1 → 이 PR(레이어 2) → #2973/#3588(창 자동 활성화, 파일 공유 없음). #3588과는 목적이 다릅니다. 저쪽은 Codex 창을 워밍업으로 켜 주는 쪽이고, 이쪽은 리셋이 일어났다고 알려 주는 쪽입니다.

이전 리뷰에서 막았던 구멍을 여섯 개로 나눠 고쳤다고 본문에 적혀 있습니다. 웹훅은 https만 받고(src/config.tsquotaResetNotifySchema), 전송은 redirect: "manual"이라 3xx를 따라가지 않습니다. 폴 바닥은 600초로 맞춰졌고, 설정값이 실제로 폴러에 들어가며, 겹치는 틱은 한 번에 하나만 돕니다. “seen” 클레임이 바로 지워지면 배달 완료로 치지 않습니다. 새 코드는 src/quota/ 아래에 모이고, 테스트는 tests/usage/quota-reset-*.test.ts 일곱 개로 등록돼 있습니다. 기본 OFF라서 아무 설정도 안 한 설치는 탐지·폴·전송을 거의 안 합니다.

보안 경계가 중요합니다. 웹훅 URL은 Slack/Discord처럼 그 자체가 비밀인 경우가 많고, ocx config show에서 가립니다. 페이로드는 account id/이메일이 아니라 accountTag(솔트 해시)와 숫자·라벨만 넣습니다. 명령 싱크는 셸 문자열이 아니라 argv 배열로 Bun.spawn합니다. tests/usage/quota-reset-core-boundary.test.ts가 요청 경로에 이 모듈이 정적 import로 붙지 않게 잠급니다. CI는 gates/changes/react-doctor 등은 통과했지만 hygiene가 빨강이고 라벨 intake: hygiene-blocked가 붙어 있습니다. 실패 이유는 missing_coauthor_credit이고, 가리키는 번호는 #2747(olddonkey)·#2766(Ingwannu)입니다. 본문이 말하는 리베이스 대상 #2783이 아니라, 히스토리/문서에 남은 옛 PR 언급이 트리거로 보입니다.

라인 - 문제

경로 hygiene / missing_coauthor_credit - #2747·#2766 Co-authored-by 또는 attribution-approved가 없어 머지 게이트가 막혀 있다. #2783 본인 리베이스와 별개로 고쳐야 한다
src/types/config.ts OcxQuotaResetNotifyConfig.pollSeconds 주석 - 아직 “floor 60”이라고 적혀 있다. 실제 MIN_POLL_SECONDS/docs-site는 600이다. 주석이 옛날 값이다
src/quota/reset-sinks.ts deliverWebhook - https·수동 리다이렉트·destination-policy 재사용은 맞다. allowPrivateNetwork=true면 루프백도 열리므로, 운영 문서에 “셀프호스트만”이라고 더 분명히 적어도 좋다
src/server/background-lifecycle.ts - 시작 시 폴러를 걸고, 실패·종료 시 stop한다. 설정 주기는 dynamic import로 맞춰 core-boundary를 지킨다
경로 베이스 #3587 - 이 PR만 dev에 직접 합치면 안 된다. 레이어 1이 먼저다
경로 #2783 - 합친 뒤 Landed via / landed-via-maintainer로 닫지 않으면 leftover가 남는다
경로 CI - hygiene 빨강 + exact-head suite 일부 진행 중. 초록 전 머지는 이르다

메인테이너의 판단이 필요한 지점

  • hygiene를 Co-authored-by로 풀지, attribution-approved로 면제할지 (대상은 olddonkey / Ingwannu의 옛 PR 언급)
  • #3587 CI·리뷰가 끝난 뒤에만 이 레이어를 올릴지
  • src/types/config.ts의 floor 60 주석을 이 PR에서 600으로 고칠지, 후속 한 줄 PR로 둘지
  • 웹훅·명령 싱크를 보안 경계 리뷰에서 한 번 더 볼지 (본문이 MAINTAINERS 노트로 이미 표시함)

너의 추천

#3587을 먼저 합친 뒤, hygiene를 풀고(권장은 관련 기여자 Co-authored-by 또는 명확한 attribution-approved), pollSeconds 주석을 600에 맞춘 다음 squash 합치세요. 합친 커밋으로 #2783Landed via #3592 at <commit>landed-via-maintainer를 달고 superseded로 닫으세요. #3588과는 독립이니 순서만 스택 문서대로 지키면 됩니다.

이 댓글은 grok-bot이 작성했습니다

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

✅ READY

  • all PR quality gates passed.

Hygiene

Deterministic PR hygiene checks passed.

@github-actions
github-actions Bot marked this pull request as draft September 5, 2026 03:14

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: adb0dc51d0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +502 to +503
webhookUrl: `http://127.0.0.1:${server.port}/hook`,
allowPrivateNetwork: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Give the activation test an HTTPS webhook

This end-to-end test writes an http:// webhook into config.json, but the newly added quotaResetNotifySchema accepts only https: URLs regardless of allowPrivateNetwork. loadConfig() therefore drops the entire optional section, syncQuotaResetActivation() returns false, and the assertion on line 515 fails before any rollover is exercised. Use a TLS-capable test receiver or inject a validated HTTPS endpoint so the focused test can pass.

AGENTS.md reference: AGENTS.md:L360-L362

Useful? React with 👍 / 👎.

Comment thread src/cli/config-command.ts
Comment on lines +21 to +26
* `webhookUrl` is here because for Slack and Discord the URL itself is the authorization:
* anyone holding it can post to the channel. It looks like configuration rather than a secret,
* which is exactly why it needs to be named explicitly — none of the other patterns match it,
* so `ocx config show` printed it and `config export` wrote it to disk in the clear.
*/
const SECRET_KEYS = /^(apiKey|key|accessToken|refreshToken|idToken|token|password|clientSecret|webhookUrl)$/i;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Redact webhook URLs in config export

Adding webhookUrl to SECRET_KEYS protects config show, get, and set, but ocx config export still serializes readConfigDiagnostics().config directly at line 198 without calling redact. Consequently, ocx config export - prints the bearer-equivalent Slack/Discord URL to stdout and file exports retain it, contrary to both this comment and the new documentation. Omit or redact this field on the export path as well.

AGENTS.md reference: AGENTS.md:L353-L359

Useful? React with 👍 / 👎.

Comment on lines +264 to +268
// Synchronous: a lost claim means a duplicate notification after restart, and claims are
// rare (one per real reset), so the write cost is irrelevant.
// A failed write is reported rather than swallowed: the caller reads true as "safe to
// dispatch", and an unpersisted claim re-notifies on the next start.
return persistNow();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Roll back an in-memory claim when persistence fails

When persistNow() fails because the config directory is temporarily unwritable or the disk is full, this function returns false but leaves the key in claims. The observer correctly refrains from dispatching that event, yet every later observation sees claims.has(key) and skips it; once writes recover, another debounced state write can even persist the never-delivered claim permanently. Delete the newly inserted claim when persistence fails so the reset can be retried.

Useful? React with 👍 / 👎.

Comment on lines +63 to +65
// Opt-in: the tick itself is a no-op unless config.quotaResetNotify is enabled with a
// sink, and the interval is unref'd, so a default install pays one dormant timer.
startQuotaResetPoller();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid starting the poller when notifications are disabled

Every server now calls startQuotaResetPoller() before checking configuration, so an installation with no quotaResetNotify section still owns a timer and wakes every 15 minutes to import the resolver and activation modules. This contradicts the new documented contract that an absent section starts no timer and makes the supposedly opt-in subsystem part of every server lifecycle. Resolve activation first and create the interval only when notification polling is enabled.

Useful? React with 👍 / 👎.

Comment on lines +50 to +51
enabled: isQuotaResetNotificationEnabled(),
events: listRecentQuotaResetEvents(limit),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return no events when limit is zero

The route and CLI explicitly accept a non-negative limit, so ?limit=0 reaches this call with zero, but listRecentQuotaResetEvents clamps its argument with Math.max(1, ...) and returns one event. Either permit zero in the store or reject it as invalid and describe the option as positive; currently callers asking for zero receive data despite their limit.

Useful? React with 👍 / 👎.

Comment thread src/quota/reset-poller.ts
Comment on lines +81 to +83
// Forced: an unforced call would be served from the 5-minute cache and produce no new
// observation at all.
await fetchProviderQuotaReports(loadConfig(), true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fence publication after an in-flight poll is stopped

The generation check runs before fetchProviderQuotaReports, even though that awaited function commits its cache and invokes the quota observer internally. If shutdown or a poller restart calls stopQuotaResetPoller() while the upstream probe is awaiting, the old generation can still publish a snapshot and dispatch a webhook after the poller was stopped. Pass cancellation/generation ownership into the probe or otherwise prevent its internal commit when the generation changes during this await.

Useful? React with 👍 / 👎.

Rebase of the maintainer's #2783 onto the Antigravity/Ollama quota layer, with the three review blockers closed as six bounded fixes: https-only webhook URLs (B1), manual redirect handling on webhook sends (B2), MIN_INTERVAL_MS and MIN_POLL_SECONDS raised together to 600s (B3), configured cadence reaching the poller through a dynamic import that keeps the core-boundary guard green (B4), an in-flight fence on poll ticks (B5), and durable seen-claims (B6). Seven new test basenames registered in the layout map and fixture under tests/usage/. Docs: server.md floor, https requirement, redirect refusal.
@lidge-jun
lidge-jun force-pushed the codex/260905-quota-reset-detection branch from adb0dc5 to 8087316 Compare September 5, 2026 03:15
@github-actions
github-actions Bot marked this pull request as ready for review September 5, 2026 03:16
@github-actions github-actions Bot removed the intake: hygiene-blocked Deterministic PR hygiene checks failed label Sep 5, 2026
Base automatically changed from codex/260905-antigravity-ollama-quota to dev September 5, 2026 03:20
@lidge-jun

Copy link
Copy Markdown
Owner Author

Maintainer admin merge (ruleset bypass recorded per MAINTAINERS.md): head 8087316; typecheck 0 and the PR's focused tests green locally; per maintainer instruction the final dev-tip CI run is the acceptance gate for this batch. wp4 of the 260905 open-work closeout.

@lidge-jun
lidge-jun merged commit 2188fca into dev Sep 5, 2026
27 of 29 checks passed
@lidge-jun
lidge-jun deleted the codex/260905-quota-reset-detection branch September 5, 2026 03:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants