[0.5.0] - 2026-05-23
The first release after the v0.4.x auth cookie lifecycle series. Headline user-facing work: a top-to-bottom CLI UX overhaul (uniform --json, exit-code policy, shell completion, stdin pipes, SIGINT-resume), auth and cookie reliability hardening (inline PSIDTS cold-start recovery, fail-closed notebooklm use, concurrent-upload safety), and the v0.3-era deprecation removal cycle. Read Breaking changes below before upgrading.
Breaking changes
Items that need attention when upgrading from 0.4.x. Full migration prose lives in the natural sections below.
NOTEBOOKLM_STRICT_DECODEnow defaults to1— RPC shape drift raisesUnknownRPCMethodError(subclass ofRPCError) at the decoder boundary instead of warning and returningNone. Set=0to opt back into the legacy behavior for one release window (the soft-mode fallback itself now emitsDeprecationWarningand is scheduled for removal in v0.6.0).rate_limit_max_retriesdefault raised from0to3with exponential-backoff fallback. Programmatic users now inherit smart-retry behavior matching the CLI. Passrate_limit_max_retries=0to restore the previous immediate-RateLimitErrorbehavior. Mutating create RPCs already opt out viadisable_internal_retries=True.server_error_max_retriesdefault raised from0to3with the same exponential-backoff fallback, covering HTTP 5xx + retryable network errors (#629). Passserver_error_max_retries=0to restore immediate failure on 5xx.max_concurrent_rpcssemaphore added with default16(#630). High-fan-out callers (e.g.asyncio.gatherover 100 RPCs) are now throttled by default instead of saturating the connection pool. Passmax_concurrent_rpcs=Noneto restore unbounded fan-out. Must satisfymax_concurrent_rpcs <= ConnectionLimits.max_connections.notebooklm use <id>fails closed when the notebook doesn't exist.usenow verifies the id withNotebooksAPI.get(id)before persisting and exits1without writing tocontext.jsonon a missing notebook / wire failure / auth-expiry. Pass--forceto bypass verification.NotebookNotFoundErrornow inherits from bothRPCErrorandNotebookError.source get/artifact get/note getexit1on not-found (was0). Matches the rest of the CLI's user-error convention so scripts can branch on the exit code.--jsonfailure body uses the standard{"error": true, "code": "NOT_FOUND", ...}envelope.generate cinematic-video --format <non-cinematic>exits2with a UsageError instead of silently overriding the conflict. Drop the conflicting flag, or usegenerate video --format <value>if a non-cinematic format was intended.NOTEBOOKLM_REFRESH_CMDdefaults toshell=False(security hardening for the shell-injection footgun when the env var is sourced from CI configs). Now parsed withshlex.splitand invoked withsubprocess.run(argv, shell=False, ...). SetNOTEBOOKLM_REFRESH_CMD_USE_SHELL=1(literal"1"only) to opt back into the legacyshell=True.source addno longer follows symlinks by default. A workspace symlink like~/Downloads/foo.pdf → /etc/passwdpreviously resolved and uploaded the target with no warning. The path now refuses symlink traversal with aClickException(exit1) unless--follow-symlinksis explicit. Scripts that point at symlink-resolved paths must add the flag (#476).- YouTube cookies no longer scraped or trusted by default at login / refresh. The cookie-domain allowlist split into REQUIRED (NotebookLM + Drive + RotateCookies) and OPTIONAL (YouTube / Docs / Mail / myaccount). Pass
--include-domains=youtube(or=all) onlogin/auth refresh --browser-cookies <browser>/auth inspectto opt YouTube back in; pass=docs/=mail/=myaccountto opt those sibling domains in explicitly (#483). - Artifact generation without
language=now honors the configured language. The Pythonclient.artifacts.generate_*methods now resolve omittedlanguageviaNOTEBOOKLM_HL/ global config /"en"instead of hard-coding"en"at the signature. Passlanguage="en"for a fixed English payload. --storage <path>no longer shares the default profile's notebook context. A previously-runnotebooklm use <id>against the default profile is invisible to a laternotebooklm --storage X.json <cmd>(and vice versa) because--storagenow derives a sibling<path>.context.json. Set the active notebook explicitly vianotebooklm --storage <path> use <id>,-n/--notebook, orNOTEBOOKLM_NOTEBOOKenv var (#467).login --browser-cookies --account EMAILnow writes the active/default profile by default instead of creating a profile from the email local-part. Use--profile-name NAMEto write a separate named profile, or--storage PATHfor an exact file. Existing profile auth for a different or unknown account prompts before overwrite (#987).- v0.3-era deprecated APIs removed —
Source.source_type,Artifact.artifact_type,Artifact.variant,SourceFulltext.source_type,StudioContentType,DEFAULT_STORAGE_PATH,notebooklm.cli.language.save_config. Migrate to the.kindproperty andnotebooklm.paths.get_storage_path(). See Removed below. - Cookie identity widened to
(name, domain, path)per RFC 6265 §5.3. Writes remain backward-compatible (flat dicts / legacy 2-tuples still accepted); reads ofauth.cookieswith the old 2-tuple key now raiseKeyError. Useauth.cookies[("SID", ".google.com", "/")],auth.flat_cookies["SID"], orauth.cookie_header.
Added
Auth and reliability
- Inline
__Secure-1PSIDTScold-start recovery. When a storage file has__Secure-1PSIDbut no__Secure-1PSIDTS, a preflight POST toaccounts.google.com/RotateCookiesmints a fresh token before any RPC traffic, so cold-start workers no longer fail on the first call. Cross-process flock serializes concurrent cold starts; respectsNOTEBOOKLM_DISABLE_KEEPALIVE_POKE=1(#865, #872). NOTEBOOKLM_BASE_URLenv var for enterprise NotebookLM deployments (#402). Routes RPC + auth traffic through a non-google.combase URL; cookie-domain allowlist auto-extends to the enterprise host. Previously enterprise users had to monkey-patch internals.NOTEBOOKLM_RPC_OVERRIDESenv-var escape hatch. When Google rotates abatchexecutemethod ID, set e.g.NOTEBOOKLM_RPC_OVERRIDES='{"LIST_NOTEBOOKS": "newId123"}'to keep working until a patch ships. Overrides are gated tonotebooklm.google.com/accounts.google.combase hosts so a redirected base can't pivot them (#486).ConnectionLimitsdataclass for httpx pool tuning. PassConnectionLimits(max_connections=200, ...)toNotebookLMClient(...)for long-running agents and high-fan-out workers — no more monkey-patching internals (#527).max_concurrent_rpcsconstructor arg (default16, #630). Bounds simultaneous in-flight RPCs to protect the connection pool under fan-out.Noneopts out — see Breaking changes for the default-shift note.--include-domainsflag onlogin/auth refresh --browser-cookies <browser>/auth inspect. Backs the REQUIRED/OPTIONAL cookie-domain split described in Breaking changes — passing=youtube/=docs/=mail/=myaccount(or=all) opts those OPTIONAL domains back in. Accepts repeated-flag or comma-separated syntax (#483).- In-memory
__Secure-1PSIDTSrecovery during--browser-cookiesextraction (#990, #991). Whenrookiepyreturns a partial cookie set (most often when the browser hasn't rotated__Secure-1PSIDTSyet), a singleRotateCookiesPOST against the live browser cookies mints the missing token before persistence. Recovery declines surface scenario-specific hints (No SID → "You are not signed in to Google in <browser>",PSIDTS missing + secondary binding intact → "RotateCookies recovery did not succeed. Open https://notebooklm.google.com in <browser>") instead of the previous generic "No valid Google authentication cookies found".
Chat
client.chat.delete_conversation(notebook_id, conversation_id)+notebooklm ask --newis now genuinely destructive (#824). Captures the web UI's "Delete history" action (J7GthcRPC) so callers can force-end a server-side conversation; the nextask()with noconversation_idstarts a brand-new thread. ⚠ Deleted turns are not recoverable. CLI prompts for confirmation;--jsonimplies--yes.notebooklm ask --newflag (previously promised in the docstring but undeclared) — starts a fresh conversation, mutually exclusive with--conversation-id.notebooklm ask --timeoutper-invocation HTTP timeout, mirroringsource add --timeout.ChatReference.answer_range+.score(#686). Every reference now exposes the answer-text span it grounds (start/end char positions) and the model's relevance score — useful for highlighting cited passages and ranking sources.chat savepreserves inline citation hover anchors (closes #660, #675). Saved notes retain[citation]-style anchors so users can hover-preview the source passage that grounded each claim in the NotebookLM web UI.
CLI ergonomics
- Uniform
--jsonenvelopes on every detail and mutating command:artifact get/rename/delete/poll/export, eightsourcesubcommands (delete/rename/refresh/clean/get/delete-by-title/add-drive/stale),note get/save/create/delete/rename,notebooklm configure, andnotebook use. Detail commands mirror the underlying dataclasses; mutating commands emit{"id": ..., "renamed|deleted|exported": true, ...}. - Standard download flag set on
download quiz/download flashcards—--all,--latest,--earliest,--name,--dry-run,--force,--no-clobber,--json— so one wrapper script works across every artifact type. - Uniform
--timeout/--intervalongenerate <kind> --wait,artifact wait, andsource wait. --limit=Nand--no-truncateon everylistcommand, plus--no-truncateonchat history.chat history --no-truncatelifts the hardcoded 50-char preview on Question/Answer columns.- Shell completion + ID-aware completers.
notebooklm completion <bash|zsh|fish>prints a completion script; once sourced,-n/--notebook,-s/--source, and-a/--artifactTAB-complete live IDs from the active profile. - SIGINT resume hint on long-running
--waitops. Ctrl-C exits 130 withCancelled. Resume with: notebooklm artifact poll <task_id>(or the parallelsource wait <source_id>) instead of dumping aKeyboardInterrupttraceback. Under--json:{"error": true, "code": "CANCELLED", "resume_hint": "..."}. - Unix
-stdin convention onask,note create,source add, and--prompt-file.echo "what is X?" | notebooklm ask -and similar pipelines now compose without temp files. NOTEBOOKLM_NOTEBOOKenv var + global--quietflag.NOTEBOOKLM_NOTEBOOK=<id> notebooklm ask "..."works without-n/--notebookor a priornotebooklm use.--quietsuppresses status output, raises the package logger floor to ERROR, and remains mutually exclusive with-v/-vv.source addwarns when a path-shaped argument doesn't exist. A typo like./missin.mdpreviously fell through to inline-text ingestion silently; an advisory stderr warning now fires before the source is added.--follow-symlinksopt-in onsource add. See Breaking changes above; scripts that point at symlink-resolved paths must add the flag to keep working (#476).source cleancommand (#261). Bulk-delete failed/stale sources in a notebook; pairs withsource stalefor inspection. Supports--all,--latest,--earliest,--dry-run, and--json.notebooklm create --useflag (#220, #413).create --use "title"makes the new notebook the active context in one step. (Plaincreateno longer auto-switches the context —--useis the explicit opt-in.)- Chromium-profile selectors on
login --browser-cookies chromium:<profile>(#648). Pick a specific Chrome user profile (e.g.chromium:Profile_1) instead of always defaulting to the first profile. Useful for users with multiple Google accounts in one browser install. auth login --updateon--all-accounts(#594). Replaces the stored state for an already-logged-in account instead of refusing on conflict.
Python API
- Source fulltext markdown format.
client.sources.get_fulltext(..., output_format="markdown")andsource fulltext -f markdown(closes #222). Requires the optionalmarkdownifyextra (pip install "notebooklm-py[markdown]"). - Public
client.rpc_call(method_id, params)(#646). A documented escape hatch for invoking anybatchexecuteRPC method directly when no high-level API wraps it yet. Pairs withNOTEBOOKLM_RPC_OVERRIDESfor community self-patching while waiting on a fix. - Observability hooks + drain API on
NotebookLMClient(#643). Newon_rpc_eventcallback (per-call timing + status),client.metricssnapshot, andawait client.drain()for graceful shutdown. Designed for long-running agents needing visibility without monkey-patching. - Correlation IDs + categorized logging (#430, #431). Every RPC carries an
X-Correlation-ID(also surfaced on log records); log records are categorized (rpc.call,rpc.retry,auth.refresh,upload.chunk, …) for filtering. Credential redaction now covers every log surface by default. - Per-call upload timeouts on
sources.add_file/add_drive(#618). Newupload_timeout/chunk_timeoutkeyword args for tuning large-file uploads against slow networks. ResearchAPI.wait_for_completion(notebook_id, task_id=None, *, timeout=1800, interval=5)(#970). Polls until research reaches a terminal state (completed/failed) or the timeout fires; passes throughtask_idon subsequent polls once the backend assigns one to prevent a later concurrent task from substituting its sources/report. Surfaces a new terminalfailedstatus so wait loops no longer spin until timeout after the backend rejects a task.notebooklm.artifacts.with_rate_limit_retry(callable, *, max_retries=3, ...)(#969). Shared retry helper for theclient.artifacts.generate_*family — catches generation-timeRateLimitError, honorsretry_after, and falls back to exponential backoff. Replaces the per-caller try/except/sleep boilerplate previously suggested indocs/python-api.md.__all__declared onnotebooklm.paths,notebooklm.migration, andnotebooklm.notebooklm_cli(#958). ADR-012 marks all three as public modules;__all__now pins the exported surface (12 names onpaths, 3 onmigration,cli+mainon the CLI entry point) sofrom notebooklm.paths import *is well-defined and the public API compatibility audit can lock it.
Changed
- Custom
--storagedownloads now use the selected auth file (#838, #888).ArtifactDownloadServicepreviously snapshotted the session's storage path at construction time, so--storageoverrides applied after construction were silently ignored on download. CLI--storageflag and mid-process profile switches are now inherited reliably. --storage <path>derives a sibling<path>.context.jsonper file (#467). Two--storageinvocations against different files no longer leak notebook state through the default profile. Precedence: explicit--storage> profile > legacy home-root. (See Breaking changes for the script-impact note.)- Conversation IDs are now server-assigned (#659, #667).
ChatAPI.ask()returns whatever the server creates instead of minting a local UUID. Previously-saved conversation IDs from a v0.4.x session remain valid against the server. - Cross-event-loop reuse fails fast with
RuntimeError(#633). OneNotebookLMClientinstance is bound to itsopen()-time event loop; reusing it from a different loop (common in hot-reload servers, worker pools) now raises on the first authed POST instead of failing with cryptic httpx errors. notebook usesurfaces the typed auth-aware error on expired credentials. Text mode shows the canonical "Not logged in" walkthrough with thenotebooklm loginremediation;--jsonemits the standardAUTH_REQUIREDenvelope.download <type>exception paths route through the typed error handler.--jsonis honored on the exception path;RateLimitError.retry_aftersurfaces as both a JSON field and a "Retry after Ns" text line;AuthErrorshows the canonical re-auth hint.notebooklm loginandnotebooklm auth refreshno longer leak Python tracebacks on unexpected failures. Unexpected exceptions become a single friendly line + bug-report URL with exit code2; original traceback remains available at-vv.--waitpaths show a transient spinner with elapsed timer and an empirical typical-duration hint where known (e.g.typically 30-40 minfor cinematic-video). No-op under--json.- CLI group docstrings synced with the live registered subcommand set.
source,download,artifact, andnotegroup--helpblocks now enumerate every registered subcommand (previously missedadd-drive,add-research,clean,wait,cinematic-video,quiz,flashcards,suggestions,rename). notebooklm --helpbins five previously-orphaned top-level commands into primary sections:auth→ Session;metadata→ Notebooks;agent/skill/language→ Command Groups.artifact pollvsartifact wait--helpclarified on ID kind.poll <task_id>straight fromgenerate;wait <artifact_id>resolved againstartifact list.- First-run profile migration no longer races concurrent invocations (#478). Previously two
notebooklminvocations starting under a fresh home (container start-up races, parallel test runs, MCP worker pools) could both run the copy-and-delete migration. Lock waits past 30 s raise a domain-specificMigrationLockTimeoutError(RuntimeError). RPCError.raw_responsepreviews capped at 80 chars;NOTEBOOKLM_DEBUG=1opts into full body. Previously embedded a 500-char preview of the upstream response — noisy in CI and capable of leaking large server payloads (#479).RPCError.rpc_idandRPCError.codedeprecations revoked. Both are now permanent aliases formethod_id/rpc_code— removing exception diagnostic aliases can mask the original exception insideexcepthandlers.- BREAKING:
note delete --jsonwithout--yesandnote renamelose-the-race now exit1(was0). Two parallel surgical fixes tocli/note.pymatching the broader--jsonexit-code convention (audit P1.T5).notebooklm note delete <id> --jsonwithout--yesnow emits{"error": true, "code": "VALIDATION_ERROR", "message": "Pass --yes to confirm deletion in --json mode", "id": ..., "notebook_id": ...}+ exit1(was the same payload as{deleted: false, error: ...}+ exit0).notebooklm note rename <id> "new"when the note vanishes between the partial-ID resolve and the underlyingget(e.g. a concurrentnote delete) now emits the standard{"error": true, "code": "NOT_FOUND", "message": "Note not found", "id": ..., "notebook_id": ...}envelope + exit1(was{renamed: false, error: ...}+ exit0). Migration: scripts branching on the exit code now correctly catch both misconfigurations; scripts parsing the JSON body must switch fromdata["deleted"] == false/data["renamed"] == falsechecks todata["error"] == true(or branch ondata["code"]).
Deprecated
await NotebookLMClient.from_storage(...)form.from_storagenow returns an awaitable async-context-manager wrapper that supports both the legacyasync with await NotebookLMClient.from_storage(...) as client:pattern (and bareawait NotebookLMClient.from_storage(...)) and the new canonicalasync with NotebookLMClient.from_storage(...) as client:pattern. Awaiting the call emitsDeprecationWarning; the await form will be removed in v1.0. Migration: drop theawaitkeyword fromasync with await NotebookLMClient.from_storage(...) as client:call sites.NotebookLMClient.rpc_callkwargs_is_retry,source_path,operation_variant. EmitDeprecationWarning; removal targets v0.6.0.NotesAPI.create_from_chat. UseChatAPI.save_answer_as_note; removal targets v0.6.0.- Positional
wait/wait_timeoutonSourcesAPI.add_url/add_text/add_file/add_drive. Calls likeclient.sources.add_url(nb_id, url, True)still work in v0.5.0 but emitDeprecationWarning; passwait=True/wait_timeout=...as keywords. Removal targets v0.6.0. CLI is unaffected. SourcesAPI.add_filemime_typeparameter. Never wired into the resumable-upload RPC — the server derives MIME from the filename extension. Passing a non-Nonevalue now emitsDeprecationWarning; removal targets v0.6.0. The separateadd_drive(..., mime_type=...)parameter is unaffected.notebooklm source add --mime-typeon the file-source path. A no-op when the resolved source type isfile; using it now emits a stderr deprecation note (suppress viaNOTEBOOKLM_QUIET_DEPRECATIONS=1). Removal targets v0.6.0. The same flag onsource add-driveis unaffected.ArtifactsAPI.wait_for_completion(poll_interval=...). Useinitial_interval=...;poll_intervalremains accepted until v0.6.0.NotebooksAPI.share(). Useclient.sharing.set_public(). Scheduled for removal in a future major release.NOTEBOOKLM_STRICT_DECODE=0soft-mode fallback. Each use emitsDeprecationWarningnaming the decoder source; the soft-mode path is scheduled for removal in v0.6.0.ResearchAPI.poll(task_id=None)default on multi-task notebooks. When multiple research tasks are in flight,poll()with notask_idnow emitsDeprecationWarning(single-task notebooks: no warning, current behavior preserved). Scheduled for removal in a future major release.
Removed
- v0.3-era deprecation cycle complete. Removed
Source.source_type,SourceFulltext.source_type,Artifact.artifact_type(use.kind);Artifact.variant(use.kind,.is_quiz,.is_flashcards);notebooklm.StudioContentType(useArtifactType);notebooklm.DEFAULT_STORAGE_PATH(usenotebooklm.paths.get_storage_path());notebooklm.cli.language.save_config(now private). - RPC raw-code
StudioContentTypealiases.notebooklm.rpc.types.StudioContentTypeandnotebooklm.rpc.StudioContentTyperemoved; useArtifactTypefor public code andArtifactTypeCodeonly for low-level RPC internals. RPCMethod.DISCOVER_SOURCESandRPCMethod.QUERY_ENDPOINT.DISCOVER_SOURCESwas an unused enum entry never exercised by anyclient.*API.QUERY_ENDPOINTwas an endpoint URL path, not a batchexecute RPC method; usenotebooklm.rpc.get_query_url()for the configured streamed-chat endpoint.
Fixed
- Artifact generation language compatibility restored. Omitting
languageon publicclient.artifacts.generate_*calls again defaults artifact output to"en"; passlanguage=Noneto opt in toNOTEBOOKLM_HLdefault-language resolution. - Source upload auth/MIME routing (#984). The resumable-upload path skipped a redundant env-auth lookup and now classifies media MIME types case-insensitively;
application/mp4is included in the media-MIME set so.mp4uploads route through the media upload path instead of the generic file path. - Source upload rejection with status
3now hints at the per-notebook source cap (#977). Previously surfaced as a bareRPCError; the error message now suggests checking the notebook source count when the server returns the cap-rejection code. - Windows atomic-replace races on cookie/profile writes (#983).
os.replaceon Windows can transiently fail withERROR_ACCESS_DENIED(5) orERROR_SHARING_VIOLATION(32) when the destination is briefly held open by AV scanners or backup software. Bounded retry with backoff handles the transient cases; persistent failures still surface. - IO event-loop blocking and chunked-download throughput (#981). Sync
Path.resolve()/open()/os.fstat()on the upload path are now wrapped inasyncio.to_thread, keeping the loop responsive under the upload semaphore on slow filesystems. Chunked downloads use a single dedicated writer thread fed by a boundedqueue.Queue(≈512 KiB buffered) instead of spawning oneto_threadcall per 64 KiB chunk. A bug whereArtifactDownloadError(raised bydownload_urls_batch()for invalid scheme / untrusted host / auth failure / HTML payload) aborted the entire batch instead of landing inDownloadResult.failedis also fixed. notebooklm login --browser-cookieshardening (#974). Tightened Chromium account enumeration, cookie-jar normalization, and refresh writes so partial extractions surface a clear error instead of silently writing an incompletestorage_state.json. Pairs with the in-memory__Secure-1PSIDTSrecovery shipped in #990 / #991.notebooklm login --browser-cookiesPlaywright account metadata (#989). The Playwright login path now writes account metadata to the profile and validates it on subsequent refresh (rejecting bool-shaped corruption from earlier buggy writes), sonotebooklm auth refresh --all-accountsand--account EMAILcan target the right profile without manual cleanup.- Playwright account metadata repair runs after the sync context exits (#1000, #1002).
notebooklm loginpreviously invokedrepair_playwright_account_metadata()whilesync_playwright()'s event loop was still active, which raised onrun_async(). The repair is now deferred until after the Playwright context closes, using the captured page HTML and saved storage path. source add-research --waittimeout path (#971). The CLI service now wraps the research wait in a typed timeout error and surfaces a resumable hint (notebooklm research poll <task_id>) instead of hanging until the global request timeout.notebooklm auth refresh --all-accountslanguage sync runs once (#976). Previously re-issued thenotebooklm.SetLanguageRPC once per account; now coalesces to a single sync at the end of the multi-account loop.- Loop-affinity guard on
sources.add_fileandclient.drain()admission (#952). Cross-event-loop reuse already failed fast on authed RPC POSTs (#633); upload admission and drain admission now also raiseRuntimeErrorinstead of silently mis-binding to the wrong loop. NotebookLMClient.close()no longer leaks the httpx pool if cancelled mid-drain (#950). ACancelledErrorraised during drain previously skippedhttpx.AsyncClient.aclose(); close now shields the transport cleanup so the connection pool is released on every cancellation path.- Deep-research source import no longer requires leaving the "Add sources?" modal (#315, #882). The deep-research flow used to discover sources but skip the modal-confirm step, leaving sources pending until a separate UI action committed them. The CLI /
ResearchAPI.import_sourcesnow commits directly. DELETE_NOTEno longer races shieldedUPDATE_NOTEat cancel time (#876). Cancellation during an in-flightNotesAPI.update(...)could land a delete before the shielded write completed, then have the update resurrect the note. Cancel-time cleanup is now ordered soDELETE_NOTEwaits for any shieldedUPDATE_NOTEto settle.- Client close preserves the original exception (#526).
NotebookLMClient.__aexit__previously masked the original body exception whenaclose()itself raised. Body exceptions are now preserved (chained via__cause__) while close-time failures still propagate; an inner shield guarantees the underlying httpx client is closed on every path. - Unique temp file per concurrent artifact download (#523). Two parallel
download_*calls against the same artifact used to share<dest>.tmpand clobber each other's bytes. Each invocation now allocates a unique temp file (PID + uuid suffix) and atomically renames into place. add_fileTOCTOU fix +max_concurrent_uploadsknob (#595).SourcesAPI.add_fileused to open the source file twice — a path swap between the two opens could substitute a different file into a successful upload. The file is now opened once; the FD is held across size check + registration + upload. Newmax_concurrent_uploads: int | None = 4onNotebookLMClientcaps simultaneous in-flight uploads (doubles as an FD-exhaustion guard forasyncio.gatherfan-outs).- Research
task_idcross-wire on concurrent in-flight tasks (#619). Two research sessions in flight on the same notebook could letResearchAPI.poll(notebook_id)silently return the latest task, mis-attributing source provenance to the caller's task.poll()gains an optionaltask_iddiscriminator;import_sources()raises the newResearchTaskMismatchError(subclass ofValidationError) when aresearch_task_idon any source disagrees with the caller'stask_id. RPCHealthsurfaceshttpxexception class name on empty error messages (#874). Somehttpxexception classes raise with emptystr(exc), which previously surfaced as a blank line. Health output now prefixes the class name (e.g.ConnectTimeout:).notebooklm logininstall hint stripped the[browser]extra (#416). Rich interpreted[browser]as a style tag, so the "Playwright not installed" message rendered aspip install "notebooklm-py"with no extras. Fixed bymarkup=False; also corrected the package name fromnotebooklmtonotebooklm-py.- Per-create-RPC idempotency hardening (#801, #806, #808, #809, #813). Six-policy idempotency registry with probe-then-retry semantics for
ADD_SOURCE,ADD_SOURCE_FILE,CREATE_NOTE,CREATE_ARTIFACT,GENERATE_MIND_MAP, andSTART_RESEARCH/IMPORT_SOURCES. Resolves duplicate-create on transient retries while still raising clear errors for genuine probe failures.
Security
- Comprehensive secret-leak audit closed across logging, auth, and URL handling (#746, #803, #903). A multi-iteration sweep tightening every surface that could leak credentials or grant codes:
payload_preview,final_url, and share-URL IDs scrubbed in error paths (#746).repr()redaction on auth objects,NOTEBOOKLM_REFRESH_CMDstdout/stderr redaction, Playwright cookie-jar domain filter, atomic profile-state writes (#803).- Standalone
__Secure-1PSIDTS/__Secure-3PSIDTS/__Secure-1PAPISID/__Secure-3PAPISIDcookie redaction in_logging.py(previously only caught insideCookie:/Set-Cookie:header values);_safe_urlredacts the URL path with/<redacted>on Google OAuth hosts (accounts.google.com,oauth2.googleapis.com,oauth2.googleusercontent.com) and subdomains, so opaque grant codes in paths like/o/oauth2/auth/<token>no longer leak throughValueErrorinterpolations or CSRF / session-id drift surfaces (#903).