feat(gmail): add --count to search with whole-query totals - #985
Conversation
Report how many results a query really has. `gmail search` and `gmail messages search` currently emit items plus nextPageToken, so a caller can tell that more results exist but not how many — and a capped page then gets read as the whole answer. Deliberately NOT Gmail's resultSizeEstimate, which is the obvious source and saturates. Measured on a live account (v0.35.0, 2026-08-12) it returned exactly 201 for every non-empty query — from:freshbooks.com (21 real matches), from:housecallpro.com (6), from:thumbtack.com newer_than:30d (3), from:honeybook.com (9) — and 0 for a query with no matches. It does not vary with maxResults either (identical at 1, 10 and 100). Surfacing it would let a caller report "3 of ~201" when the truth is 3 of 6, which is worse than reporting nothing. Instead --count asks for one maximal page of bare ids (maxResults=500, fields=<items>/id,nextPageToken) and counts them. Exact when the set fits a page, reported as totalMatches; a lower bound when the page fills with more behind it, reported as totalMatchesAtLeast, so a saturated probe can never be mistaken for a total. Exactness holds for the narrow queries where a wrong count does the most damage. Opt-in, so no caller pays the extra round-trip without asking. The text path prints to stderr, keeping stdout parseable. Refs #983
…ered Follow-up to the --count review on #983, which asked for an explicit contract around --page, --all and --results-only. Two of the three were real defects: --results-only unwraps the envelope to the bare result array, so a count field cannot survive it. The probe ran anyway and its result was silently discarded — a Gmail request spent for nothing, with no way for the caller to tell. It is now skipped, and the caller is told on stderr rather than left guessing. --all has already walked every page, so the items in hand ARE the whole set. The probe was asking Google a question the walk had just finished answering. The total now comes from the walk, which also makes it exact by construction. --page needed no code change but did need saying: the count is always for the WHOLE query, never the remainder after a cursor. That is the number that stops a caller concluding an absence, and keeping it page-independent means it does not drift while paging. Now stated in the flag help. Refs #983
|
🦞👀 Pull request received. I will update this pull request when review starts. |
|
Codex review: needs maintainer review before merge. Reviewed August 13, 2026, 6:25 AM ET / 10:25 UTC. ClawSweeper reviewWhat this changesAdds Merge readiness✅ Ready for maintainer review Keep this collaborator-authored PR open for normal maintainer landing review. The focused opt-in feature is correct on source review, preserves parseable stdout, and includes credible live Gmail evidence. Priority: P3 Review scores
Verification
How this fits togetherGog’s Gmail search commands fetch paged thread or message results and format them for JSON or table output. With flowchart LR
A[CLI query and flags] --> B[Gmail search command]
B --> C[Fetch requested page or pages]
C --> D{Count requested?}
D -->|No| E[Format results]
D -->|Yes| F[IDs-only whole-query probe]
F --> G[Exact total or lower bound]
G --> E
E --> H[JSON or parseable table output]
Before mergeNone. Agent review detailsSecurityNone. Review metrics
Root-cause clusterRelationship: Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Technical reviewBest possible solution: Land the additive Do we have a high-confidence way to reproduce the issue? Not applicable: this PR adds an optional command flag rather than repairing a current-main failure; its claimed live Gmail runs cover the proposed behavior. Is this the best way to solve the issue? Yes. An IDs-only first-page probe produces exact small-set totals while honestly labeling broad results as lower bounds, without relying on Gmail’s unreliable result estimate. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 0d8088534f09. LabelsLabel changes:
Label justifications:
EvidenceWhat I checked:
Likely related people:
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
|
…nitized get (#272) Raises `MIN_GOG_VERSION` **0.35.0 → 0.37.0** (and `fly-gog-runner/Dockerfile`'s pin with it) and takes up what 0.36.0 / 0.37.0 added. ## New tools **Gmail — draft-side reply/forward** (gog 0.36.0, openclaw/gogcli#977) | Tool | | |---|---| | `gog_gmail_drafts_reply` | Save a reply as a draft — inherited recipients, subject and quote; never sends | | `gog_gmail_drafts_reply_all` | Same, to every participant | | `gog_gmail_drafts_forward` | Save a forward as a draft; `to` is optional, so it can be staged with no recipients at all | They take the same flag set as the send commands and share gog's composition path, so `replySchema` / `appendReplyFlags` are reused verbatim rather than re-declared. Staging a reply previously meant `gog_gmail_drafts_create` + `replyToThreadId`, which threads the draft but inherits neither the original's recipients nor its quoted body — both had to be rebuilt by hand, and a missed Cc is invisible until the draft goes out. **Sheets — Connected Sheets reads** (gog 0.37.0, openclaw/gogcli#938) `gog_sheets_datasource_list` / `_describe`, and `gog_sheets_datasource_table_list` / `_describe` / `_read`. Read-only by construction (gog exposes no create/update/refresh/delete here). A data-source table has no id of its own in the Sheets API — its definition lives on its top-left cell — so extracts are addressed by a sheet-qualified A1 anchor like `Extracts!B3`. ## Behaviour these depend on - **`gog_gmail_get` gains `sanitizeContent`.** The flag predates 0.37.0 but emitted the headers and body **twice** in JSON (openclaw/gogcli#992), so the flag meant to shrink the payload enlarged it. Verified against a live 0.37.0 build: the sanitized message now arrives under a single `message` key. - **`gog_calendar_events` gains `days`**, and it and `gog_calendar_search` now describe the window rules gog 0.36.0 enforces (openclaw/gogcli#981). `--days` is a window *length* anchored at `--from`; it used to discard `--from` silently and answer for today, at exit 0, in a well-formed table. Fixed presets no longer combine with `from`/`to`/`days`, and `days` no longer combines with `to`. The old descriptions invited exactly the combinations that now fail, and the base calendar test asserted an arg array gog refuses to run. - **The auth tools gain `extraScopes`** (plus `--force-consent` on the interactive one, since Google re-prompts for a *new* scope only when consent is forced). Nothing else can request `bigquery.readonly`, which Google demands whenever a Sheets response *contains* BigQuery Connected Sheets data — without it the new sheets tools cannot be authorized through the wrapper at all. Documented in `docs/auth-scopes.md`. ## Not adopted: `gmail search --count` gog 0.36.0 (openclaw/gogcli#985) upstreamed this wrapper's match-count probe, down to the page size and the exact/lower-bound split. The local probe stays, and the stale comment claiming gog *cannot* supply the count is corrected to say why: - it is spent only on a result set already known to be truncated, where `--count` is decided before the search runs and would cost every search an extra Gmail request; - it is best-effort, where gog returns the probe's error from the whole command — a failed count would turn a search that *did* succeed into an error. ## Verification Beyond the unit tests (the mocked suites only assert arg arrays), every new command was run against the real gog v0.37.0 binary with fake ids, which parses flags before any API call: - `gmail drafts reply` / `reply-all` / `forward` — full flag sets parse; failures are the API's 400 on the fake id (and a send-as validation for `--from`), not unknown flags. - `sheets datasource list` / `describe` / `table list` / `table describe` / `table read` — all reach the API and 404 on the fake spreadsheet. - `calendar events --from 2026-09-25 --days 5` returns the anchored window; `--today --from …` and `--days … --to …` are both rejected with the messages the tool descriptions now quote. - `gmail get --sanitize-content --json` on a real message: top-level keys are exactly `["message"]`. `npm test` (all workspaces + fly-gog-runner), `npm run typecheck` and `npm run build` are green; the 100% coverage gate holds. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01EjH6C4jVKavpHiFwBW574N Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Lands #984 with @chrischall's two commits preserved, plus one maintainer fix.
The feature is right:
--counton gmail search/messages reports how manyresults a query really has, spending a probe only when the answer is not
already known. Two correctness gaps fixed on top:
--all --page TOKEN --countcounted only the results after the cursor,not the whole query.
Gmail's
resultSizeEstimateis an estimate by contract, and the output saysso — broad queries report
totalMatchesAtLeast, never a fabricated exact.Live read-only proof against the approved test mailbox:
Closes #984. Closes #983. Thanks @chrischall.