• v0.31.0 814da4878d

    v0.31.0 — G2: the Gmail tenant can write, and a move is not a move
    All checks were successful
    CI / gate (push) Successful in 1m26s
    CI / image (push) Successful in 16s
    Stable

    joeyr released this 2026-08-22 13:59:41 -05:00 | 8 commits to main since this release

    G2. The Gmail tenant can write. update_message, move_message and delete_message,
    behind MAIL_WRITABLE_FOLDERS and registered nowhere when it is empty — which it still is
    in the stack file, because the setting does not go in until the live pass has run.

    The tool list changed, so reconnect the Gmail connector. Claude caches tool
    descriptions per connector and does not refetch them because a container restarted.

    Added

    • update_message(message_id, read, starred). Two optional flags, neither defaulted;
      passing neither is an error rather than a no-op reported as success. starred, where
      the mail tenant says flagged
      — Apple's clients draw a flag, Gmail draws a star and
      has a Starred view a user asks for by name. Probe 10g confirmed the IMAP \Flagged bit
      and the [Gmail]/Starred label are the same thing, so the rename costs nothing
      underneath.

    • move_message(message_id, destination_folder), and it says what it really does.
      Gmail has labels, not folders: this removes the source label and adds the destination,
      and the message KEEPS every other label it had and stays in All Mail. The description
      says so in those words, because a model that reports "moved" to a user who then finds
      the message under another label has told them something false about their own mailbox.

      The destination goes through the same four-tier resolver the read tools got in 0.29.7,
      so spam, trash and Sent Mail all land correctly — and a name matching nothing is
      an error, never a silent success. The source does not: it comes out of a handle this
      server minted, and running that through a fuzzy matcher would let a forged near-miss
      resolve onto a real label.

    • delete_message(message_id), trash-only, no second argument, and none ever. Gmail's
      delete_message is born the way 0.30.0 made the mail tenant's, so the two connectors
      never mean two things by one name. expunge_message is G5's.

    Changed

    • A move here is ONE command. icloudmail moves in three — COPY, flag \Deleted,
      scoped UID EXPUNGE — because iCloud advertises no MOVE. Gmail does, probe 10b issued
      one and read a COPYUID back, so that workaround is not ported. Its intermediate state
      — a message flagged deleted but not expunged, hidden in most clients while still
      counting against the label — cannot arise here.

    • _trash_folder resolves the \Trash role FIRST on this tenant, the opposite order
      from next door
      , and the inversion is load-bearing rather than tidy. TRASH_FOLDER is
      shared by both mail tenants and defaults to Apple's Deleted Messages, which is not a
      label on any Gmail account — so config-first would send every delete to a label that
      does not exist, or, if a user happens to have made one by that name, to an ordinary
      label Gmail's Bin view never shows while the tool reported success. Written up in
      CLAUDE.md under A role folder resolves the way the provider guarantees it.

    • The server instructions stop claiming to be read-only when they are not. The
      sentence is now built from the same setting the tools are registered on, so the promise
      in the initialize response and the surface behind it cannot drift apart.

    • New preflight check gmail_trash, silent when writes are off. Not a restatement of
      mail_writable: that asks whether the write allowlist is readable, this asks whether
      the one label every delete lands in is reachable at all. A deployment can pass every
      other check and still refuse every delete.

    Notes

    • probes/gmail_probe_write.py ran first, and G2 was scoped without it. Probes 1–9
      read CAPABILITY, proved COPY additive and proved an expunge inside a label removes the
      label — but never issued a MOVE, never read a COPYUID, and never touched Trash,
      which is three of the four things these tools are built out of. Findings in
      docs/gmail-plan.md under Probe 10. The two that changed the code:

      A COPY to [Gmail]/Trash removes the source label, unlike a COPY to any other
      label, so a delete really does take the message out of the mailbox and the description
      may say so. And Trash HIDES a message's other labels rather than stripping them — a
      two-label message put in Trash was invisible in both while X-GM-LABELS still reported
      them, and moving it back out brought the untouched one with it. That is what lets
      delete_message promise a reversible delete instead of hedging.

    • Two of five new tests passed against deliberately broken code, and both were fixed
      rather than trusted. One asserted a true thing the broken version also satisfied; the
      other could not reach its own hazard because the fake ASSIGNED where imaplib APPENDS.
      The rule is now in CLAUDE.md under A new guard is not proved until it has been
      broken
      .

    • Not yet run live. G2's pass is written up in docs/operations.md beside M1–M5 and
      G1 and is owed before this is trusted on real mail — including the absence check, which
      no unit test can confirm on the deployed service.

    Downloads