Skip to content

feat(gmail): add --thread-id to drafts create/update for parity with send#674

Merged
steipete merged 2 commits into
openclaw:mainfrom
chrischall:feat/gmail-drafts-thread-id
Jun 3, 2026
Merged

feat(gmail): add --thread-id to drafts create/update for parity with send#674
steipete merged 2 commits into
openclaw:mainfrom
chrischall:feat/gmail-drafts-thread-id

Conversation

@chrischall

Copy link
Copy Markdown
Contributor

Closes #673.

Problem

gmail send accepts --thread-id ("uses latest message for headers"), but gmail drafts create/update only accepted --reply-to-message-id. There was no thread-aware reply path when composing a draft: a caller who knows the thread (but not the latest message id) had nowhere to put it, and passing a thread id into --reply-to-message-id silently mis-threads — the draft lands in the thread via subject fallback but In-Reply-To/References never anchor to the parent's RFC822 Message-Id, with no error.

Change

Wire --thread-id into GmailDraftsCreateCmd and GmailDraftsUpdateCmd, reusing the existing prepareComposeReplyfetchReplyInfo thread path (which already selects the latest thread message for reply metadata). Narrow, matches send:

  • Mutually exclusive with --reply-to-message-id (use only one of …, same guard as send).
  • --quote now accepts a thread target too.
  • Update: a caller-provided --thread-id overrides the draft's existing thread; the existing-draft thread fallback is preserved when --thread-id is omitted (no behavior change for current callers).
  • thread_id added to the dry-run/JSON payload for both commands.
  • Regenerated docs/commands/gog-gmail-drafts-{create,update}.md.

Tests

  • TestGmailDraftsCreateCmd_WithThreadID — resolves the thread's latest message; asserts the posted draft's threadId and decoded raw In-Reply-To: <latest@id> + References.
  • TestGmailDraftsCreateCmd_ThreadIDAndMessageIDMutuallyExclusive — guard.
  • TestGmailDraftsUpdateCmd_WithThreadIDOverridesExisting — caller thread overrides the draft's existing thread.
  • go test ./internal/cmd green; make lint 0 issues; make docs-commands clean.

Real-behavior proof (redacted live Gmail)

Built from this branch (v0.21.1-…+dirty), against a real account:

$ gog gmail drafts create --subject 'threadid-live-test (scratch)' \
      --body '…' --thread-id 19e867c34415b3ee --json
{ "draftId": "r3188009555946395473",
  "message": { "id": "19e8686f9dba26a9", "labelIds": ["DRAFT"], "threadId": "19e867c34415b3ee" },
  "threadId": "19e867c34415b3ee" }

# draft headers (drafts get):
In-Reply-To: <chrischall/gogcli/check-suites/CS_kwDOSibav88AAAAQu0J3Pg/[email protected]>
References:  <chrischall/gogcli/check-suites/CS_kwDOSibav88AAAAQu0J3Pg/[email protected]>

# thread's latest message Message-Id:
<chrischall/gogcli/check-suites/CS_kwDOSibav88AAAAQu0J3Pg/[email protected]>
MATCH: True

The draft's threadId equals the supplied --thread-id, and its In-Reply-To/References equal the thread's latest message's RFC822 Message-Id — exactly the anchoring that was missing before. Scratch draft was deleted afterward.

(Reported via the gogcli-mcp wrapper, which currently resolves thread→latest-message client-side; once this ships the wrapper passes --thread-id through directly.)

…send

`gmail send` accepts --thread-id ("uses latest message for headers") but
`gmail drafts create`/`update` only accepted --reply-to-message-id, so there
was no thread-aware reply path when composing a draft. Callers who knew the
thread (not the latest message id) had nowhere to put it; passing a thread id
into --reply-to-message-id silently mis-threads — the draft lands in the thread
via subject fallback but In-Reply-To/References never anchor to the parent's
RFC822 Message-Id, and there's no error.

Wire --thread-id into both draft commands, reusing the existing
prepareComposeReply/fetchReplyInfo thread path (selects the latest thread
message for reply headers):

- Mutually exclusive with --reply-to-message-id (matches `send`).
- --quote now accepts a thread target too.
- On update, a caller-provided --thread-id overrides the draft's existing
  thread; the existing-draft thread fallback is preserved when omitted.
- dry-run/JSON gains a thread_id field.

Tests: create resolves to the thread's latest message and stamps threadId +
In-Reply-To/References; mutual-exclusion guard; update override. Regenerated
command docs.

Live-verified (redacted): `gog gmail drafts create --thread-id <T>` produced a
draft whose In-Reply-To/References equal the thread's latest message Message-Id
and whose threadId == <T> (scratch draft created, verified, deleted).

Closes openclaw#673.
@clawsweeper

clawsweeper Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed June 3, 2026, 4:50 PM ET / 20:50 UTC.

Summary
The PR adds --thread-id to gmail drafts create and gmail drafts update, updates generated command docs and dry-run payloads, and adds draft-thread unit coverage.

Reproducibility: yes. Source inspection shows current main supports --thread-id on gmail send but not on draft create/update, and the provided live Gmail proof demonstrates the missing draft anchoring behavior the PR fixes.

Review metrics: 3 noteworthy metrics.

  • Changed surface: 5 files changed, 216 added, 8 removed. The diff is focused on one Gmail draft command surface plus generated docs and tests.
  • Commands extended: 2 draft commands. Both create and update receive the new user-facing --thread-id option, matching the existing send command concept.
  • Regression tests: 3 tests added. Coverage exercises create threading, mutual exclusion, and update override behavior before merge.

Merge readiness
Overall: 🦞 diamond lobster
Proof: 🦞 diamond lobster
Patch quality: 🦞 diamond lobster
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Next step before merge

  • No repair lane is needed because this review found no actionable blocker; remaining work is normal maintainer review and merge handling.

Security
Cleared: The diff touches Gmail CLI command code, generated command docs, tests, and a release note; no secret handling, dependency, workflow, or supply-chain surface changes were found.

Review details

Best possible solution:

Review and land this PR or an equivalent narrow change so draft create/update can target a Gmail thread through the existing reply helper; keep the linked issue open until the implementation merges.

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

Yes. Source inspection shows current main supports --thread-id on gmail send but not on draft create/update, and the provided live Gmail proof demonstrates the missing draft anchoring behavior the PR fixes.

Is this the best way to solve the issue?

Yes. Reusing prepareComposeReply/fetchReplyInfo is the narrowest maintainable path because it keeps draft behavior aligned with the existing send implementation and preserves the current update fallback when no thread is supplied.

AGENTS.md: found and applied where relevant.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 096323d506b9.

Label changes

Label justifications:

  • P2: This is a normal Gmail CLI feature/parity improvement with limited blast radius.
  • rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body includes redacted live Gmail terminal output showing the after-fix draft thread ID and reply headers matching the target thread's latest message.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes redacted live Gmail terminal output showing the after-fix draft thread ID and reply headers matching the target thread's latest message.
Evidence reviewed

What I checked:

  • Current main lacks draft thread-id flags: Current main has ReplyToMessageID on Gmail draft create/update but no ThreadID field in those command structs, while gmail send already has ThreadID. (internal/cmd/gmail_drafts.go:258, 096323d506b9)
  • Existing send path already supports thread replies: Current main normalizes --thread-id, rejects combining it with --reply-to-message-id, and routes it through prepareComposeReply/fetchReplyInfo for latest-thread-message reply metadata. (internal/cmd/gmail_send.go:67, 096323d506b9)
  • PR reuses existing compose reply path: The PR adds ThreadID to both draft commands, preserves mutual exclusion with --reply-to-message-id, includes thread_id in dry-run data, and passes the selected thread through the existing draft compose input. (internal/cmd/gmail_drafts.go:259, dc45e34c18c5)
  • Update fallback remains intentional: The PR keeps the existing draft-thread fallback when no caller thread is supplied and only overrides it when --thread-id is provided. (internal/cmd/gmail_drafts.go:625, dc45e34c18c5)
  • Focused regression coverage: The PR adds tests for draft create with a thread ID, create mutual exclusion, and draft update overriding an existing thread with a caller-supplied thread. (internal/cmd/gmail_drafts_cmd_test.go:1408, dc45e34c18c5)
  • Real behavior proof in PR body: The PR body includes redacted live Gmail terminal output showing a draft created with --thread-id, matching draft/thread IDs, and In-Reply-To/References matching the latest message's RFC822 Message-Id. (93e062b1fbff)

Likely related people:

  • steipete: The available current-main blame for Gmail draft/send/reply code points to the v0.21.0 release commit authored by Peter Steinberger, and the PR branch includes a release-note commit from the same person. (role: recent area contributor; confidence: medium; commits: 2c84d8981dd5, 93e062b1fbff; files: internal/cmd/gmail_drafts.go, internal/cmd/gmail_send.go, internal/cmd/gmail_reply.go)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

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.

How this review workflow works
  • 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.

@clawsweeper clawsweeper Bot added proof: sufficient Contributor real behavior proof is sufficient. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P2 Normal priority bug or improvement with limited blast radius. labels Jun 2, 2026
@steipete
steipete merged commit 22fbc35 into openclaw:main Jun 3, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦞 diamond lobster Very strong PR readiness with only minor 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.

gmail drafts create/update: add --thread-id for parity with gmail send

2 participants