Gmail send_email returns JSONDecodeError after successful delivery, causing duplicate emails on retry

Open 💬 1 comment Opened Aug 14, 2026 by itcasim0

What version of the Codex App are you using (From “About Codex” dialog)?

26.803.8161.0

Gmail plugin version observed locally: 0.1.8

What subscription do you have?

Pro (Codex desktop app authenticated with a ChatGPT account)

What platform is your computer?

Microsoft Windows NT 10.0.19045.0 x64 (Windows 10 Home, build 19045)

What issue are you seeing?

The Gmail send_email action reports an error even though Gmail has already accepted and sent the message successfully.

The returned tool result is:

JSONDecodeError: Expecting value: line 1 column 1 (char 0)
error_code: UNKNOWN
isError: true

Immediately checking the Gmail Sent mailbox shows that the message was delivered with the expected recipient, subject, attachment filename, and attachment size.

Because the action is reported as failed after the non-idempotent side effect has completed, retrying the tool sends a duplicate email. In this occurrence, two calls both returned the same JSONDecodeError, and two separate messages appeared in Sent.

This is especially risky for autonomous agents because a normal retry policy can duplicate external writes while the tool result claims failure.

Recipient addresses and raw Gmail message/thread IDs are omitted for privacy.

What steps can reproduce the bug?

  1. In the Windows Codex desktop app, connect the Gmail app/plugin.
  2. Call the Gmail send_email action with a multipart/mixed payload containing:
  • a UTF-8 text/plain body;
  • one application/zip attachment supplied through body.base64_url_content;
  • attachment size: 2,160,477 bytes;
  • response_fields: snippet and size_estimate.
  1. Observe that the tool returns:

``text
JSONDecodeError: Expecting value: line 1 column 1 (char 0)
``

  1. Search Gmail Sent for the exact recipient and subject.
  2. Observe that the message exists in Sent and has the expected ZIP attachment and exact byte size.
  3. Invoke the same action again after treating the first result as a failure.
  4. Observe the same tool error and a second successfully sent email.

The issue reproduced on both consecutive calls during the same Codex task on 2026-08-14 (Asia/Seoul).

What is the expected behavior?

After Gmail accepts the message, send_email should return a successful tool result.

If response parsing fails after the send side effect, the connector should surface an indeterminate-delivery state rather than a definite failure, and ideally provide an idempotency/deduplication mechanism so retries cannot send duplicates.

Additional information

The MIME payload was valid enough for Gmail to preserve and send the attachment correctly. This does not appear to be an OAuth or permission failure because both messages are present under the SENT label with the expected attachment metadata.

The failure appears to occur after delivery, while the connector is decoding or normalizing the downstream response. The exact reason the downstream response was empty or non-JSON is not visible from the client-side tool result.

View original on GitHub ↗

1 Comment

safal207 · 14 days ago

This looks like a useful boundary case where execution outcome and observation/response outcome are being collapsed into one status.

A causal view of the failing path is:

logical send intent
  -> execution attempt
  -> Gmail accepts/commits the message
  -> response normalization / decoding
  -> JSONDecodeError
  -> caller-visible FAILED
  -> retry
  -> duplicate external effect

The first meaningful divergence is not the JSON parse failure by itself. It is the mapping of a post-commit observation failure to an execution failure. For a non-idempotent tool, that makes ordinary retry unsound.

A minimal durable result contract would distinguish at least:

FAILED_BEFORE_EFFECT
COMMITTED
UNKNOWN_AFTER_EFFECT

Then retry semantics become much safer:

  • FAILED_BEFORE_EFFECT -> retry may be allowed.
  • COMMITTED -> return success / receipt.
  • UNKNOWN_AFTER_EFFECT -> reconcile before any retry (e.g. provider receipt/message ID when available, Sent-state lookup, or a caller-supplied idempotency/correlation key).

It would also help to keep a stable logical_operation_id separate from the concrete attempt_id, so recovery after an ambiguous result is still bound to the same user intent rather than being treated as a fresh send.

A regression test I would add at the connector/tool-contract layer:

  1. provider side effect succeeds;
  2. response decode/normalization fails afterward;
  3. tool returns UNKNOWN_AFTER_EFFECT, not definite failure;
  4. caller recovery reconciles the existing effect;
  5. total externally visible sends remains exactly 1.

And the inverse control: a failure provably before the provider side effect remains retry-safe.

There are related pattern-level reports in #36592 (create_thread succeeds but reports failure), #27283 (GitHub comment retry duplicates after uncertain outcome), and #35894 (successful mutation can lose the response race and be reported as failure). I am not claiming the same root cause, but they appear to share the same higher-level invariant: a side-effecting operation must not return a definitive failure once the effect may already have committed.