Skip to content

fix(notebooks): detect quota exhaustion on Discovery Engine code 7/8, not just code 3 (#1546)#1574

Open
teng-lin wants to merge 2 commits into
mainfrom
fix/create-notebook-quota-rpc-codes
Open

fix(notebooks): detect quota exhaustion on Discovery Engine code 7/8, not just code 3 (#1546)#1574
teng-lin wants to merge 2 commits into
mainfrom
fix/create-notebook-quota-rpc-codes

Conversation

@teng-lin

@teng-lin teng-lin commented Jun 14, 2026

Copy link
Copy Markdown
Owner

Summary

notebooks.create() converts a quota-shaped CREATE_NOTEBOOK RPC failure into the actionable NotebookLimitError ("delete old notebooks and try again"). The trigger was gated on a single gRPC status code — CREATE_NOTEBOOK_QUOTA_RPC_CODE = 3 (INVALID_ARGUMENT), established as the legacy backend's quota code in #328.

#1546 surfaced that Google's newer "Discovery Engine" backend reports notebook-quota exhaustion as gRPC code 7 (PERMISSION_DENIED), not 3. The reporter (caichengle666) confirmed: hit a notebook limit, the AzXHBd create returned status=7, and freeing space fixed it. The code-3-only filter misses that, so affected accounts get a raw, confusing RPC error instead of the clear limit guidance.

This widens the gate to a set of quota-suspect codes: CREATE_NOTEBOOK_QUOTA_RPC_CODES = frozenset({3, 7, 8}).

Related Issue

Related to #1546.

Changes

  • src/notebooklm/_notebooks.py: replace the scalar CREATE_NOTEBOOK_QUOTA_RPC_CODE = 3 with CREATE_NOTEBOOK_QUOTA_RPC_CODES = frozenset({3, 7, 8}) and switch the membership check in _raise_quota_error_if_detected from != to not in. Codes: 3 = legacy INVALID_ARGUMENT (fix(settings): use account limits for create quota errors #328), 7 = Discovery Engine PERMISSION_DENIED (CREATE_NOTEBOOK RPC CCqFvf returns status=3 (Invalid argument) since Jun 10 #1546), 8 = canonical gRPC RESOURCE_EXHAUSTED (insurance for if Google ever adopts the "correct" code). Comments updated to explain the multi-cohort code situation and why the gate is safe.
  • tests/unit/test_notebook_api.py: extend the TestCreateNotebookQuotaDetection cluster — code 7 at/over limit raises NotebookLimitError; code 8 at/over limit raises NotebookLimitError; code 7 well under limit does NOT convert and the raw RPCError propagates (the critical false-positive guard, mirroring the existing code-3 under-limit test). Existing code-3 behavior unchanged.
  • CHANGELOG.md: entry under ### Fixed (Unreleased).

Why broadening the code set is safe (no false positives)

The code check only gates the quota path — it never decides it. After the code matches, _raise_quota_error_if_detected verifies against the account's real limit: it fetches account_limits.notebook_limit via GET_USER_SETTINGS and only raises NotebookLimitError when owned_count >= max(notebook_limit - 1, 0). That count-vs-limit gate is the true disambiguator, so widening the code set cannot cause a false NotebookLimitError unless the account is genuinely at/near its limit.

Concretely, a code-7 failure on an account well under its limit (i.e. a genuine permission error, not quota) still propagates the raw RPCError unchanged — proven by the new test_create_permission_denied_under_limit_preserves_rpc_error regression test. The same protection covers code 3 (which also fires on wire-format drift) and code 8.

The renamed constant is private to the _notebooks module (not in __all__, not re-exported, not imported by tests), so the scalar→frozenset rename is not a public-API or test-seam break — confirmed by scripts/audit_public_api_compat.py (no new allowlist entries).

Test Plan

  • I tested these changes locally
  • Tests pass (full uv run pytest: 10206 passed, 69 skipped, 1 xfailed)
  • Linting and formatting pass (uv run ruff check . + uv run ruff format --check ., whole tree)
  • Type checking passes (uv run mypy src/notebooklm --ignore-missing-imports)
  • Public API compat audit passes with no new allowlist entries (uv run python scripts/audit_public_api_compat.py)
  • If this PR changes architectural shape, an ADR has been added or updated. — N/A (behavioral bug fix, no architectural change)

Notes

🤖 Generated with Claude Code

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved notebook quota exhaustion handling on both legacy and Discovery Engine backends by recognizing additional quota-related gRPC failures and showing user-friendly notebook limit guidance instead of raw RPC errors.
    • Continues to preserve the original RPC error when the account is still under its notebook limit (quota guidance is shown only when the owned-count meets the configured limit).
  • Tests
    • Expanded unit coverage for quota detection when creating notebooks at and under quota, including numeric and string forms of the backend status codes.
  • Documentation
    • Added troubleshooting guidance for notebook/source limits, including a tier table for each plan.

@teng-lin

Copy link
Copy Markdown
Owner Author

@claude review

@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 30d656cd-627e-4c60-820f-46fc93a5b37c

📥 Commits

Reviewing files that changed from the base of the PR and between c8d42d9 and f7b1f7c.

📒 Files selected for processing (1)
  • docs/troubleshooting.md
✅ Files skipped from review due to trivial changes (1)
  • docs/troubleshooting.md

📝 Walkthrough

Walkthrough

The single CREATE_NOTEBOOK_QUOTA_RPC_CODE = 3 integer constant in _notebooks.py is replaced with CREATE_NOTEBOOK_QUOTA_RPC_CODES = frozenset({3, 7, 8}), alongside a new _normalize_rpc_code helper to safely coerce the error's rpc_code field into an int for membership testing. _raise_quota_error_if_detected switches from equality to membership testing. Four new async test methods cover gRPC codes 7 and 8 at-limit and under-limit cases. Troubleshooting documentation explains quota limits to users. A changelog entry documents the Discovery Engine quota detection.

Changes

Quota RPC Code Expansion

Layer / File(s) Summary
Quota RPC codes constant and normalization helper
src/notebooklm/_notebooks.py
CREATE_NOTEBOOK_QUOTA_RPC_CODE (int 3) is replaced by CREATE_NOTEBOOK_QUOTA_RPC_CODES (frozenset({3, 7, 8})). A new _normalize_rpc_code helper coerces RPCError.rpc_code (typed `str
Detection function membership testing
src/notebooklm/_notebooks.py
_raise_quota_error_if_detected gates CREATE_NOTEBOOK failures on whether the normalized rpc_code is in CREATE_NOTEBOOK_QUOTA_RPC_CODES, replacing prior equality to a single code. Docstring and comments updated to describe the "gate then verify" behavior.
Test coverage, helper updates, troubleshooting docs, and changelog
tests/unit/test_notebook_api.py, docs/troubleshooting.md, CHANGELOG.md
Test helper _create_invalid_argument_error accepts `str

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • teng-lin/notebooklm-py#552: Implements the create-flow probe-then-retry workflow that routes NotebooksAPI.create() through _raise_quota_error_if_detected, the same function updated by this PR.
  • teng-lin/notebooklm-py#328: Earlier PR expanding CREATE_NOTEBOOK quota-exhaustion handling to convert RPC failures into NotebookLimitError based on server-derived account notebook limits; this PR extends that logic to additional gRPC status codes.

Poem

🐇 Hippity-hop, the quota codes grew,
From one lonely 3 to a frozenset crew!
Codes seven and eight now join in the check,
No more raw RPC errors to leave you a wreck.
The Discovery Engine is finally seen —
The fluffiest fix that there ever has been! 🌸

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically summarizes the main change: expanding quota exhaustion detection from code 3 to also recognize codes 7 and 8 on the Discovery Engine backend.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/create-notebook-quota-rpc-codes

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 and usage tips.

@claude

claude Bot commented Jun 14, 2026

Copy link
Copy Markdown

Claude finished @teng-lin's task in 1m 25s —— View job


Code Review

  • Gather context and understand the request
  • Read changed files and diff
  • Review src/notebooklm/_notebooks.py
  • Review tests/unit/test_notebook_api.py
  • Review CHANGELOG.md
  • Post review feedback

This is a well-scoped, well-tested fix. The two-stage gate design (code set membership → count-vs-limit verification) is the right architecture — the broad code matching is safe precisely because the count check does the real disambiguating. A few observations below, ordered by impact.

Logic (src/notebooklm/_notebooks.py)

No issues. The change is minimal and correct:

# Before
or error.rpc_code != CREATE_NOTEBOOK_QUOTA_RPC_CODE

# After
or error.rpc_code not in CREATE_NOTEBOOK_QUOTA_RPC_CODES

frozenset is the right choice for an immutable module-level constant — O(1) membership test, clearly signals "don't mutate this." The count-vs-limit gate at _notebooks.py:560 (owned_count < max(notebook_limit - 1, 0)) is unmodified and remains the true disambiguator.

The updated comment block (_notebooks.py:34–49) is unusually thorough for a constant definition, but in this codebase where undocumented gRPC codes are the #1 breakage class, that level of explanation is justified.

Tests (tests/unit/test_notebook_api.py)

The three new tests cover the critical paths:

Test Code Count Expected
test_create_permission_denied_at_limit_raises_limit_error 7 500/500 NotebookLimitError
test_create_resource_exhausted_at_limit_raises_limit_error 8 500/500 NotebookLimitError
test_create_permission_denied_under_limit_preserves_rpc_error 7 20/500 raw RPCError

Minor gap: There is a false-positive guard test for code 7 under limit but not for code 8. Since code 8 is explicitly described as "insurance" (not yet observed in the wild), this is understandable — but a symmetrical test would complete the picture:

@pytest.mark.asyncio
async def test_create_resource_exhausted_under_limit_preserves_rpc_error(self):
    original = _create_invalid_argument_error(rpc_code=8)
    api = _make_api(rpc_call=AsyncMock(side_effect=original))
    _set_account_limit(api, 500)
    api.list = AsyncMock(return_value=_owned_notebooks(20))

    with pytest.raises(RPCError) as exc_info:
        await api.create("Genuine Exhausted Error")

    assert exc_info.value is original
    assert not isinstance(exc_info.value, NotebookLimitError)

Add this test →

Pre-existing nit (not introduced by this PR): _create_invalid_argument_error(rpc_code=7) still emits the hardcoded message "status code 3 (Invalid argument)" even when called with rpc_code=7 or rpc_code=8. The tests only assert on the rpc_code field and exception identity, so this doesn't affect correctness, but it does make the error messages confusing when a test fails.

CHANGELOG

Entry is accurate, well-structured, and appropriately explains both what changed and why broadening the code set is safe. The false-positive explanation is directly useful to future readers diagnosing a bug.

Hold note

The PR description flags code 7 as pending verification on #1546 — confirmed as the right call to mark as "Related to" rather than "Closes." If it turns out code 7 is not quota-specific, dropping it from the frozenset is a one-line edit and the count-vs-limit gate guarantees no false NotebookLimitError was ever raised in the interim.


Summary: The core change is correct and the coverage is strong. The one meaningful gap is the missing code-8 false-positive guard test (suggested above). Everything else is either clean or pre-existing. Ready to merge once the hold-note verification on #1546 is complete.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request expands notebook-quota exhaustion detection to support the "Discovery Engine" backend by matching a set of gRPC status codes {3, 7, 8} instead of just the legacy code 3. The reviewer suggests including string representations of these status codes in the CREATE_NOTEBOOK_QUOTA_RPC_CODES set to ensure robust matching, as RPCError.rpc_code can sometimes be populated as a string.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/notebooklm/_notebooks.py
@teng-lin teng-lin force-pushed the fix/create-notebook-quota-rpc-codes branch from 358ce64 to 9f2dadd Compare June 14, 2026 08:05
… not just code 3 (#1546)

`notebooks.create()` converts a quota-shaped CREATE_NOTEBOOK failure into the
actionable `NotebookLimitError`, but the trigger was gated on a single gRPC
status code (3 / INVALID_ARGUMENT — the legacy backend's quota code from #328).
Google's newer "Discovery Engine" backend reports the same notebook-limit
exhaustion as code 7 (PERMISSION_DENIED), so affected accounts got a raw,
confusing RPC error instead of the "delete old notebooks" guidance (#1546).

Replace the scalar `CREATE_NOTEBOOK_QUOTA_RPC_CODE = 3` with
`CREATE_NOTEBOOK_QUOTA_RPC_CODES = frozenset({3, 7, 8})` (3 = legacy
INVALID_ARGUMENT, 7 = Discovery Engine PERMISSION_DENIED, 8 = canonical gRPC
RESOURCE_EXHAUSTED as insurance) and switch the membership check to `not in`.

This cannot cause false positives: a matched code only *gates* the quota path;
the existing count-vs-limit verification (fetch the account's advertised
notebook_limit, raise only when owned_count is at/near it) is the real
disambiguator. A code-7 failure on an account well under its limit (a genuine
permission error) still propagates unchanged — covered by a new regression test
mirroring the existing code-3 under-limit case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@teng-lin teng-lin force-pushed the fix/create-notebook-quota-rpc-codes branch from 9f2dadd to c8d42d9 Compare June 14, 2026 08:09
@teng-lin

Copy link
Copy Markdown
Owner Author

Thanks @claude — addressed both suggestions in c8d42d9:

  1. Code-8 false-positive guard: added test_create_resource_exhausted_under_limit_preserves_rpc_error (code 8, 20/500), symmetrical to the code-7 under-limit guard, so both broad codes are proven to propagate the raw RPCError when the account is under its limit.
  2. Helper message nit: _create_invalid_argument_error now interpolates the actual rpc_code into the message (...status code {rpc_code}.) instead of the hardcoded "status code 3", so a failing assertion shows the real status (the helper is now exercised with 3/7/8/"7").

Also addressed gemini-code-assist's str | int | None typing concern (string-form rpc_code) by normalizing the code to int before the membership check (new _normalize_rpc_code helper + test_create_string_form_quota_code_at_limit_raises_limit_error).

Noted on the hold: keeping code 7 in the set and marked "Related to" #1546 pending the verification there.

@claude

claude Bot commented Jun 14, 2026

Copy link
Copy Markdown

Claude finished @teng-lin's task in 2m 6s —— View job


Code Review (follow-up on c8d42d9)

  • Gather context and understand the request
  • Read changed files to verify follow-up changes
  • Verify _normalize_rpc_code helper
  • Verify new tests (code-8 under-limit guard, string-form test, helper message fix)
  • Post final review

All three items from the previous review have been addressed correctly. Here's a brief verification of each.

_normalize_rpc_code helper (_notebooks.py:53–71)

The implementation is clean and covers every meaningful case:

Input Result Behavior
7 (int) 7 membership match ✓
"7" (str) 7 normalizes string form ✓
"USER_DISPLAYABLE_ERROR" None ValueErrorNone not in {3,7,8} → early return ✓
None None explicit guard → early return ✓

Catching both TypeError and ValueError is correct — TypeError covers pathological inputs like int([7]) should the type annotation ever be violated at runtime; ValueError covers non-numeric strings. The docstring rationale (mirroring _app.errors._normalized_rpc_code but kept local due to the _app > client-runtime layering constraint) is accurate. CHANGELOG line 120–121 also covers this.

Missing code-8 false-positive guard (test_notebook_api.py:459–472)

test_create_resource_exhausted_under_limit_preserves_rpc_error is present and symmetric to the code-7 guard: code 8, 20/500, asserts exc_info.value is original and not isinstance(..., NotebookLimitError). Closes the gap.

String-form test (test_notebook_api.py:382–397)

test_create_string_form_quota_code_at_limit_raises_limit_error uses rpc_code="7" with a 500/500 account and asserts NotebookLimitError — exactly what's needed to pin the normalization path. Good.

Helper message nit (test_notebook_api.py:65–74)

_create_invalid_argument_error now interpolates {rpc_code} into the message string and accepts str | int — so a failing assertion on a code-7 or code-8 test shows the actual status code, not the hardcoded "3". Clean fix.


Summary: All feedback addressed correctly. The normalization approach is better than embedding string forms in the frozenset (fewer representations to keep in sync, handles any future coercion edge cases). The test coverage is now symmetric across all three quota codes in both the at-limit and under-limit directions. Ready to merge pending the #1546 verification on code 7.

#1546)

Add a notebook/source-limit reference to the "RPCError: [3]" / quota section:
distinguishes the notebooks-per-account cap from the sources-per-notebook cap
(a common confusion), notes the library auto-raises NotebookLimitError for the
notebook case, and lists the documented per-tier limits with a link to Google's
official limits page. Companion to the quota-detection change in this PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants