Skip to content

feat(gmail): add --count to search and messages search - #984

Closed
chrischall wants to merge 2 commits into
openclaw:mainfrom
chrischall:feat/gmail-search-count
Closed

feat(gmail): add --count to search and messages search#984
chrischall wants to merge 2 commits into
openclaw:mainfrom
chrischall:feat/gmail-search-count

Conversation

@chrischall

Copy link
Copy Markdown
Contributor

Closes #983.

gmail search and gmail messages search emit items plus nextPageToken, so a caller can tell that more results exist but not how many. That gap is where agent/LLM callers go wrong: a capped page reads as the complete answer, and the caller reports that a message doesn't exist when it does.

Why not resultSizeEstimate

It's the obvious source, and it saturates. Measured on a live account, v0.35.0 (402def5), 2026-08-12:

query resultSizeEstimate true count
from:freshbooks.com 201 21
from:housecallpro.com 201 6
from:thumbtack.com newer_than:30d 201 3
from:honeybook.com 201 9
zzzznomatch 0 0

201 for every non-empty query, and identical at maxResults 1, 10 and 100 — so it isn't a per-page figure either. It's a has-results boolean wearing a number's clothes. Emitting it would let a caller report "3 of ~201" when the truth is 3 of 6, which is worse than reporting nothing.

What --count does

One extra list call for a single maximal page of bare ids:

maxResults=500, fields="threads/id,nextPageToken"   (messages/id for messages search)
  • set fits the page → exact total → totalMatches
  • page fills with more behind it → honest lower bound → totalMatchesAtLeast

Two separate field names so a saturated probe can never be mistaken for a total. Exact for the narrow queries where a wrong count does the most damage.

Opt-in, so nobody pays the round-trip without asking. Text path prints to stderr, keeping stdout parseable.

Live verification (bin/gog from this branch)

$ ./bin/gog gmail search from:freshbooks.com --max=3 --count --json
   returned: 3 | totalMatches: 21           ground truth (--all): 21  ✅

$ ./bin/gog gmail search from:housecallpro.com --max=3 --count --json
   returned: 3 | totalMatches: 6            ground truth (--all): 6   ✅

$ ./bin/gog gmail messages search from:honeybook.com --max=2 --count --json
   returned: 2 | totalMatches: 9            ground truth (--all): 9   ✅

$ ./bin/gog gmail search invoice --max=3 --count --json
   returned: 3 | totalMatchesAtLeast: 500   (large set — lower bound)  ✅

Opt-in respected — without --count, no count fields and no probe:

$ ./bin/gog gmail search from:housecallpro.com --max=2 --json
   count fields: NONE

Text path — hint on stderr, table clean on stdout:

$ ./bin/gog gmail search from:housecallpro.com --max=2 --count --plain
stderr: Showing 2 of 6 matches.
stdout: 3 rows, contains "matches." 0 times

Tests

8 new tests in internal/cmd/gmail_search_count_test.go via the existing httptest idiom — exact count, lower bound, probe shape (asserts ids-only fields, maxResults=500, and that it reuses the search's own query), opt-in (probe must not run without the flag), zero-match, messages-search variant, and the stderr wording.

make lint clean (0 issues); go test ./internal/cmd/ passes (81s).

Notes

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 openclaw#983
@clawsweeper

clawsweeper Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

@clawsweeper clawsweeper Bot added P3 Low-risk cleanup, docs, polish, ergonomics, or speculative feature. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Aug 13, 2026
@clawsweeper

clawsweeper Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Codex review: found issues before merge. Reviewed August 12, 2026, 11:51 PM ET / August 13, 2026, 03:51 UTC.

ClawSweeper review

What this changes

The PR adds an opt-in --count flag to Gmail thread and message searches, reporting an exact count up to 500 results or an explicit lower bound for larger queries.

Merge readiness

⚠️ Ready for maintainer review - 3 items remain

Keep open: the focused implementation and live Gmail proof are credible, but a maintainer must choose whether the new count belongs on text output as a stderr hint, and the generated command references still omit the new flag.

Priority: P3
Reviewed head: ca114661c6b903214bbd65521281e93b900a45d8
Owner decision: Required. See Decision needed.

Review scores

Measure Result What it means
Overall readiness 🐚 platinum hermit (4/6) The patch is focused and backed by live Gmail evidence and httptest coverage; generated command references and a product-contract choice remain.
Proof confidence 🐚 platinum hermit (4/6) Sufficient (terminal): The PR body provides copied terminal output from live Gmail runs showing exact counts, saturated lower bounds, opt-in behavior, and clean table stdout.
Patch quality 🐚 platinum hermit (4/6) 1 actionable review finding remain.

Verification

Check Result Evidence
Real behavior Verified Sufficient (terminal): The PR body provides copied terminal output from live Gmail runs showing exact counts, saturated lower bounds, opt-in behavior, and clean table stdout.
Evidence reviewed 6 items Current-main behavior: The current Gmail thread search command has paging and JSON-envelope output but no count flag, so this remains a new capability rather than an already-landed fix.
Count implementation: The PR probes Gmail with an ID-only 500-result request and marks the result exact only when Gmail supplies no next-page token.
Follow-up edge handling: The PR head skips an unusable probe for results-only output and reuses the completed all-page walk as the exact total.
Findings 1 actionable finding [P3] Regenerate the command reference for --count
Security None None.

How this fits together

Gmail search commands turn a query and paging options into Gmail list requests, then render JSON or a table for CLI and agent callers. The new path optionally derives a whole-query count before rendering the result envelope or stderr hint.

flowchart LR
  Query[Search query and flags] --> List[Fetch Gmail result page]
  List --> Requested{Count requested?}
  Requested -->|No| Render[Render JSON or table]
  Requested -->|Yes| Resolve[Probe IDs or reuse all-page results]
  Resolve --> Envelope[Add exact total or lower bound]
  Envelope --> Render
Loading

Decision needed

Question Recommendation
Should --count ship for both JSON and table output, with table mode emitting its count only as a stderr hint? Accept stderr hints: Keep the current cross-format behavior, which mirrors existing pagination hints while leaving table stdout byte-parseable.

Why: The implementation is opt-in and preserves stdout parsing, but the linked feature discussion explicitly leaves this user-facing output contract to maintainers.

Before merge

  • Regenerate the command reference for `--count` (P3) - The two generated Gmail search pages still omit this new public flag, even though each page directs contributors to run make docs-commands. Include the regenerated references so CLI users can discover the option.
  • Complete next step (P2) - A maintainer must settle the new output contract; the remaining generated-doc update is then mechanical.

Findings

  • [P3] Regenerate the command reference for --countinternal/cmd/gmail_search.go:22
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Implementation and coverage production +180/-8, tests +361 across 4 files The feature adds a small shared count helper with substantially more focused httptest coverage than production code.

Root-cause cluster

Relationship: fixed_by_candidate
Canonical: #983
Summary: The linked open issue defines the same missing whole-query count; this PR is its concrete implementation candidate.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Technical review

Best possible solution:

Choose the cross-format contract, then retain the ID-only probe design and publish the generated references for both Gmail search commands.

Do we have a high-confidence way to reproduce the issue?

Not applicable as a bug reproduction: this is a new optional count capability, and the PR supplies live Gmail after-fix output for its intended behavior.

Is this the best way to solve the issue?

Unclear until maintainers choose the text-output contract; the ID-only bounded probe is otherwise a narrow, honest approach that avoids Gmail's unreliable estimate.

Full review comments:

  • [P3] Regenerate the command reference for --countinternal/cmd/gmail_search.go:22
    The two generated Gmail search pages still omit this new public flag, even though each page directs contributors to run make docs-commands. Include the regenerated references so CLI users can discover the option.
    Confidence: 0.98

Overall correctness: patch is correct
Overall confidence: 0.9

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 0d8088534f09.

Labels

Label justifications:

  • P3: This is an opt-in CLI feature with a narrow, non-urgent Gmail search enhancement.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body provides copied terminal output from live Gmail runs showing exact counts, saturated lower bounds, opt-in behavior, and clean table stdout.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body provides copied terminal output from live Gmail runs showing exact counts, saturated lower bounds, opt-in behavior, and clean table stdout.

Evidence

What I checked:

  • Current-main behavior: The current Gmail thread search command has paging and JSON-envelope output but no count flag, so this remains a new capability rather than an already-landed fix. (internal/cmd/gmail_search.go:16, 0d8088534f09)
  • Count implementation: The PR probes Gmail with an ID-only 500-result request and marks the result exact only when Gmail supplies no next-page token. (internal/cmd/gmail_search_count.go:48, ca114661c6b9)
  • Follow-up edge handling: The PR head skips an unusable probe for results-only output and reuses the completed all-page walk as the exact total. (internal/cmd/gmail_search_count.go:105, ca114661c6b9)
  • Generated docs remain stale: Both generated Gmail search reference pages instruct contributors to run make docs-commands, but the PR changes only four internal command/test files and neither reference page lists --count. (docs/commands/gog-gmail-search.md:3, 0d8088534f09)
  • Feature history: Peter Steinberger has repeatedly maintained the Gmail search surfaces, including typed list presentation and system-label search handling; Chris Hall also has a prior merged Gmail listing contribution. (internal/cmd/gmail_messages.go:29, bbb084ca8296)
  • Real behavior proof: The PR body records after-fix runs against a live Gmail account for exact thread/message counts, a saturated lower bound, opt-in behavior, and stderr-only table output. (ca114661c6b9)

Likely related people:

  • Peter Steinberger: Current-main history shows repeated Gmail search, list-presentation, and query-filter maintenance. (role: long-running Gmail search contributor; confidence: high; commits: bbb084ca8296, 7f1ef97e156d; files: internal/cmd/gmail_search.go, internal/cmd/gmail_messages.go, internal/cmd/gmail_search_request.go)
  • chrischall: A prior merged Gmail listing change touched the affected message-search surface, beyond this proposed branch. (role: recent Gmail listing contributor; confidence: medium; commits: 3c9466af5171; files: internal/cmd/gmail_messages.go)

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Have a maintainer choose the JSON-only versus stderr-hint contract.
  • Regenerate and include both Gmail search command-reference pages.

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (1 earlier review cycle)
  • reviewed 2026-08-13T03:37:33.315Z sha 23cf42f :: found issues before merge. :: [P3] Regenerate the command reference for the new flags

…ered

Follow-up to the --count review on openclaw#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 openclaw#983
@chrischall

Copy link
Copy Markdown
Contributor Author

Pushed ca11466 addressing the two open questions from the review on #983.

Both turned out to be real defects rather than just undocumented behaviour, verified against a live account before fixing:

--results-only was spending a request for nothing. unwrapPrimary emits the bare result array, and threads/messages are both in its known-result list — so the count field was computed and then silently dropped. Confirmed live: --count --results-only returned the plain array with no count anywhere, having made the extra Gmail call regardless. The probe is now skipped and the caller is told on stderr instead of being left to guess.

--all was asking Google a question it had just answered. The walk has already exhausted every page, so the items in hand are the whole set. Confirmed live: --all --count on a 6-thread query reported totalMatches: 6 — correct, but via a redundant round-trip. The total now comes from the walk, which makes it exact by construction rather than by probe.

--page needed no code change but did need stating. Confirmed live: on page 2 of a 6-thread query, totalMatches was 6, not the 4 remaining. That is deliberate — "how many match this query" is the number that stops a caller reporting an absence, and keeping it page-independent means it does not drift while paging. Now said outright in the flag help.

On the product question in the review — JSON-only vs. also text: happy to defer entirely. Worth one data point for the decision: the text path currently prints Showing 2 of 6 matches. to stderr, which is the same channel and shape as the existing printNextPageHintWithAll hint right beside it, so stdout stays byte-identical and parseable either way. If you'd rather it were strictly JSON-only, deleting the printGmailMatchCount call is a two-line change and I'll push it on request.

Two new tests cover the skip paths, including one that would fail if a probe result could masquerade as an --all total. make lint clean (0 issues), go test ./internal/cmd/ passing.

Noting the clawsweeper:no-new-fix-pr / needs-product-decision labels: this PR is not trying to jump the queue on that decision — it's here as a working reference implementation with live proof attached, and I'm happy for it to sit until a maintainer picks the contract (or to close it if the answer is no).

@chrischall

Copy link
Copy Markdown
Contributor Author

Live proof for ca11466, on a real account with bin/gog built from this branch (v0.35.0-based, 2026-08-13):

--results-only — probe skipped, caller told, stdout unchanged

$ ./bin/gog gmail search from:housecallpro.com --max=2 --count --json --results-only
stderr: --count has no effect with --results-only: the count is an envelope field,
        and --results-only emits only the result array. Skipping the extra request.
stdout: starts with '[' (bare array), occurrences of "totalMatches": 0

Before this commit the same command made the extra Gmail request and then discarded its result silently.

--all — total from the walk

$ ./bin/gog gmail search from:housecallpro.com --all --count --json
returned: 6 | totalMatches: 6

Same answer as before the change, now without the redundant round-trip.

On what each layer proves. The live runs above confirm the observable contract. They cannot themselves prove the request did not fire — gog has no HTTP-debug flag to count calls — so that is asserted deterministically in the unit tests instead, where the httptest handler increments a counter on any ids-only probe and both new tests require it to stay at zero. TestGmailSearch_Count_SkipsProbeWithAll additionally serves a probe response of 99 items, so if the probe ever fires again the reported total becomes 99 instead of 2 and the test fails loudly rather than silently passing.

Earlier live proof for the core feature is unchanged and still in the PR description: exact counts of 21, 6 and 9 all matching --all ground truth, and a lower bound of 500 on a large set.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P3 Low-risk cleanup, docs, polish, ergonomics, or speculative feature. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(gmail): report how many results a search really has — resultSizeEstimate saturates at 201 and is unusable as a count

1 participant