Skip to content

Improving Integrations and Agentic Developement to better match the usecase. #431

Draft
ajspig wants to merge 16 commits intomainfrom
abigail/dev-1372-new
Draft

Improving Integrations and Agentic Developement to better match the usecase. #431
ajspig wants to merge 16 commits intomainfrom
abigail/dev-1372-new

Conversation

@ajspig
Copy link
Copy Markdown
Contributor

@ajspig ajspig commented Mar 17, 2026

Vibecoding page — Integrator. This is "use Claude Code (or Cursor, etc.) to build a Honcho integration faster."

  • Agent skills that guide the integration (/honcho-integration)
  • MCP/CLI for quick testing/debugging during development

Guides overview — Adopter. "use Honcho in Claude Code". Needs to feel like a menu of "here's how to use Honcho in X" rather than developer docs

Summary by CodeRabbit

  • Documentation
    • Reorganized navigation: swapped two guide group labels and relocated several integration pages between groups
    • Moved n8n off the Integrations list and added a v2 n8n guide under Application Interfaces
    • Added Gmail and Granola integration guides with end-to-end import workflows and example scripts
    • Expanded MCP server documentation with multi-client setup and configuration examples
    • Updated Agentic Development guide and overview content; added session design guidance about peer join order
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 17, 2026

Walkthrough

This PR reorganizes documentation navigation, renames and relocates several Guides groups/pages, expands the MCP integration guide, restructures vibecoding content, and adds comprehensive Gmail and Granola integration guides plus their example importer scripts.

Changes

Cohort / File(s) Summary
Docs navigation
docs/docs.json
Renamed Guides groups, moved pages between groups, adjusted v2/v3 integration entries (removed n8n from v2 Integrations; added v2/guides/n8n to Application Interfaces).
Overview & MCP guide
docs/v3/guides/overview.mdx, docs/v3/guides/integrations/mcp.mdx
Reworked overview layout and cards; expanded MCP guide into hosted-server approach with prerequisites, tool lists, headers table, and multi-client setup examples/config snippets.
Vibecoding / intro
docs/v3/documentation/introduction/vibecoding.mdx
Rewrote metadata and content: renamed to "Agentic Development", replaced plugin-focused setup with npx/manual instructions, added MCP Server and tooling/API tables.
New integration guides
docs/v3/guides/gmail.mdx, docs/v3/guides/granola.mdx
Added Gmail guide (OAuth, thread ingestion, mappings, examples) and Granola guide (MCP OAuth, meeting/transcript parsing, interactive import modes, helpers).
Example importer scripts
examples/gmail/honcho_gmail.py, examples/granola/honcho_granola.py
Added Gmail and Granola importer scripts with OAuth flows, API calls, parsing, peer/session creation, message building, and Honcho loading; many helper functions and data classes included.
Core concepts docs
docs/v3/documentation/core-concepts/design-patterns.mdx
Added note that peer join order matters and examples for incremental peer addition/idempotency.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Script
    participant GmailAPI as Gmail API
    participant Honcho
    User->>Script: run honcho_gmail.py (credentials, query)
    Script->>GmailAPI: list_threads/query
    GmailAPI-->>Script: thread IDs
    Script->>GmailAPI: fetch thread messages
    GmailAPI-->>Script: thread messages
    Script->>Script: decode/strip/normalize peers & build session/messages
    Script->>Honcho: create peers/sessions and send messages
    Honcho-->>Script: ack
    Script-->>User: report summary
Loading
sequenceDiagram
    participant User
    participant Script
    participant GranolaMCP as Granola MCP
    participant Honcho
    User->>Script: run honcho_granola.py (auth)
    Script->>GranolaMCP: DCR + PKCE auth -> token
    GranolaMCP-->>Script: access token
    Script->>GranolaMCP: list_meetings / get_transcript
    GranolaMCP-->>Script: meeting metadata + transcripts
    Script->>Script: parse participants/turns, review decision
    Script->>Honcho: create session/peers and send messages (two-person or summary)
    Honcho-->>Script: ack
    Script-->>User: final transfer summary
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • VVoruganti

Poem

🐰 With twitching whiskers I hop in delight,

I stitched Gmail and Granola into the site.
Pages rearranged, MCP set to hum,
Scripts fetch the threads and transcripts come.
A rabbit's cheer — docs finished just right!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title refers to 'Integrations' and 'Agentic Development,' which are central to the PR's changes (guides restructuring, new integration docs, MCP expansion), but is imprecise due to grammatical error ('Developement') and vague phrasing ('better match the usecase').

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch abigail/dev-1372-new
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

@ajspig ajspig marked this pull request as ready for review March 17, 2026 21:28
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/v3/documentation/introduction/vibecoding.mdx`:
- Around line 64-67: Update this page so the MCP contract matches
docs/v3/guides/integrations/mcp.mdx: change the advertised base URL from
https://mcp.honcho.dev/mcp to https://mcp.honcho.dev and replace shorthand tool
names in the "Workspace", "Peers", "Sessions", and "Conclusions" lists (e.g.
`get/set_workspace_metadata`, `add_messages`, `inspect_session`) with the
explicit tool identifiers used in the MCP guide (use the exact function names
from the guide for workspace metadata, message addition, session inspection,
etc.) so copy/paste setup and agent instructions are identical across both
pages.

In `@docs/v3/guides/gmail.mdx`:
- Around line 605-607: The Card component's copy and link are incorrect: update
the card body text from "See how the Granola integration maps to common Honcho
patterns." to reference Gmail (e.g., "See how the Gmail integration maps to
common Honcho patterns."), and change the absolute href value on the Card
(currently "/v3/documentation/core-concepts/design-patterns") to a relative
Mintlify path (e.g., "../../core-concepts/design-patterns") so the link works
from docs/v3/guides/gmail.mdx; locate the Card invocation (Card title="Design
Patterns" icon="cubes" href="...") and update the inner copy and href
accordingly.

In `@docs/v3/guides/overview.mdx`:
- Around line 14-27: The Card components in this snippet use absolute hrefs
(e.g., href="/v3/guides/integrations/claude-code") which cause Mintlify 404s;
update each Card's href to a relative path from docs/v3/guides/overview.mdx
(e.g., change href="/v3/..." to the appropriate ../../guides/... or similar
relative path) for the entries referencing Claude Code, MCP Server, Hermes
Agent, OpenClaw, and Agent Zero (look for the Card components and their href
attributes) and apply the same relative-path fix to the other Card blocks
indicated (lines 41-49, 56-64, 71-72).

In `@examples/gmail/honcho_gmail.py`:
- Around line 282-290: The dry-run preview assumes each thread in
all_thread_messages has at least one message, but fetch_thread_messages() can
return an empty list when get_thread() returns {}; update the dry-run block
(inside the args.dry_run branch) to skip threads with empty msgs before
accessing msgs[0]—i.e., check if not msgs: continue—so the loop safely ignores
failed/empty thread payloads and only builds body_preview/prints for threads
where m = msgs[0] exists.
- Around line 36-48: The find_credentials function only searches the current
working directory, which breaks when the script is run from elsewhere; update
find_credentials to also search the script's directory (the directory containing
honcho_gmail.py) for files matching "client_secret*.json" before raising
FileNotFoundError. Use the module file location (e.g., via __file__ /
Path(__file__).resolve().parent) along with globbing to prefer a match in the
script directory, then fall back to os.getcwd() (or vice versa) so the helper
finds credentials placed next to the script; keep the same FileNotFoundError
message if no matches are found.
- Around line 123-132: The current regex-based extract_email and extract_name
break on valid RFC 5322 headers (e.g., quoted commas and encoded-word names);
replace the regex logic by first decoding RFC2047 words using
email.header.decode_header and make_header and then parsing addresses with
email.utils.parseaddr / getaddresses: in extract_email decode the header, call
parseaddr to get the email, lowercase and strip it; in extract_name decode the
header, call parseaddr to get the display name, strip and fall back to the
original header when empty; also add a helper like parse_address_list that
decodes the header and uses getaddresses to return cleaned "Name <addr>" or addr
entries and handle empty headers gracefully.

In `@examples/granola/honcho_granola.py`:
- Around line 71-87: The callback currently accepts any code without verifying
the OAuth state; update the flow to validate a per-run state by adding an
expected_state attribute (e.g., _OAuthCallback.expected_state) that is set when
you build the authorize URL, then change _OAuthCallback.do_GET to read the
"state" param from the query and reject the callback if it's missing or does not
match expected_state (set auth_result["error"] and return a 400 with an
explanatory message). Ensure you still populate auth_result["code"] only after
state verification and keep responses (200/400) and headers consistent.
- Around line 665-666: The workspace ID literal is incorrect: change the Honcho
instantiation to use workspace_id="granola" (replace the "granola_test" string
wherever Honcho(workspace_id="granola_test") appears, e.g., the honcho =
Honcho(...) at the top and the other occurrence near the later block), and
update the matching workspace mention in the guide file
docs/v3/guides/granola.mdx so the documented workspace ID and the completion
banner both read "granola" consistently.
- Around line 276-283: The current broad except in the retry loop around
call_mcp_tool and extract_mcp_text swallows all failures and returns None;
narrow the handling by catching specific exceptions (e.g., network/transport
errors, auth errors, and a NotFound/NoTranscript error) instead of Exception:
keep retrying on transient HTTP/transport errors (and log them), surface or
re-raise authentication/contract errors (or log and abort) so they don't
silently fall back to a summary-only transcript, and only return None when you
detect an explicit "no transcript" condition from call_mcp_tool or
extract_mcp_text; reference the retry loop, call_mcp_tool, extract_mcp_text and
the except block to implement these changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6ecdc626-2653-4e92-aff4-46de1ee254b5

📥 Commits

Reviewing files that changed from the base of the PR and between 24f94f3 and 13497ed.

📒 Files selected for processing (9)
  • docs/docs.json
  • docs/v2/guides/n8n.mdx
  • docs/v3/documentation/introduction/vibecoding.mdx
  • docs/v3/guides/gmail.mdx
  • docs/v3/guides/granola.mdx
  • docs/v3/guides/integrations/mcp.mdx
  • docs/v3/guides/overview.mdx
  • examples/gmail/honcho_gmail.py
  • examples/granola/honcho_granola.py
Comment on lines +64 to +67
- **Workspace:** `inspect_workspace`, `list_workspaces`, `search_workspace`, `get/set_workspace_metadata`
- **Peers:** `create_peer`, `list_peers`, `chat`, `get_peer_card`, `get_peer_context`, `get_representation`, `search_peer_messages`
- **Sessions:** `create_session`, `list_sessions`, `clone_session`, `add_messages`, `get_session_context`, `inspect_session`, `get_session_summaries`
- **Conclusions:** `list_conclusions`, `query_conclusions`, `create_conclusions`, `delete_conclusion`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep the MCP contract consistent with docs/v3/guides/integrations/mcp.mdx.

This page advertises https://mcp.honcho.dev/mcp and shorthand tool names like get/set_workspace_metadata, add_messages, and inspect_session, but the dedicated MCP guide in the same PR documents https://mcp.honcho.dev and the explicit tool names. One of the two pages is now wrong for copy/paste setup or agent instructions.

Also applies to: 72-80

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/v3/documentation/introduction/vibecoding.mdx` around lines 64 - 67,
Update this page so the MCP contract matches
docs/v3/guides/integrations/mcp.mdx: change the advertised base URL from
https://mcp.honcho.dev/mcp to https://mcp.honcho.dev and replace shorthand tool
names in the "Workspace", "Peers", "Sessions", and "Conclusions" lists (e.g.
`get/set_workspace_metadata`, `add_messages`, `inspect_session`) with the
explicit tool identifiers used in the MCP guide (use the exact function names
from the guide for workspace metadata, message addition, session inspection,
etc.) so copy/paste setup and agent instructions are identical across both
pages.
Comment on lines +14 to +27
<Card title="Claude Code" icon="terminal" href="/v3/guides/integrations/claude-code">
Long-term memory that survives context wipes, session restarts, and project switches
</Card>
<Card title="LangGraph" icon="diagram-project" href="/v3/guides/integrations/langgraph">
Add persistent memory and theory of mind to your LangGraph agents
<Card title="MCP Server" icon="star-of-life" href="/v3/guides/integrations/mcp">
Add Honcho memory to Claude Desktop, Cursor, Windsurf, Cline, and any MCP client
</Card>
<Card title="Hermes Agent" icon="bolt" href="/v3/guides/community/hermes">
Cross-session memory for Nous Research's Hermes agent
</Card>
<Card title="OpenClaw" icon="lobster" href="/v3/guides/integrations/openclaw">
Memory across every channel — WhatsApp, Telegram, Discord, Slack, and more
</Card>
<Card title="Agent Zero" icon="triangle" href="/v3/guides/community/agent0">
Persistent memory plugin for the Agent Zero framework
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use relative hrefs for the new guide cards.

These new cards point at internal docs pages via absolute /v3/... paths. This site has already hit Mintlify 404s with absolute cross-references, so these should stay relative from docs/v3/guides/overview.mdx.

Based on learnings: In the Honcho documentation site (Mintlify-based), relative paths like ../../guides/... are the correct pattern for cross-references between documentation sections, not absolute paths like /v2/guides/... which can cause 404s.

Also applies to: 41-49, 56-64, 71-72

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/v3/guides/overview.mdx` around lines 14 - 27, The Card components in
this snippet use absolute hrefs (e.g.,
href="/v3/guides/integrations/claude-code") which cause Mintlify 404s; update
each Card's href to a relative path from docs/v3/guides/overview.mdx (e.g.,
change href="/v3/..." to the appropriate ../../guides/... or similar relative
path) for the entries referencing Claude Code, MCP Server, Hermes Agent,
OpenClaw, and Agent Zero (look for the Card components and their href
attributes) and apply the same relative-path fix to the other Card blocks
indicated (lines 41-49, 56-64, 71-72).
Comment on lines +123 to +132
def extract_email(from_header: str) -> str:
"""Extract bare email from 'Name <email>' format."""
match = re.search(r"<([^>]+)>", from_header)
return match.group(1).lower() if match else from_header.lower().strip()


def extract_name(from_header: str) -> str:
"""Extract display name from 'Name <email>' format."""
match = re.match(r'^"?([^"<]+)"?\s*<', from_header)
return match.group(1).strip() if match else from_header.strip()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd honcho_gmail.py

Repository: plastic-labs/honcho

Length of output: 1855


🌐 Web query:

email.utils parseaddr Python documentation quoted display names

💡 Result:

email.utils.parseaddr() parses an address header value into (realname, email_address) and returns ('', '') on failure (with strict=True rejecting malformed input by default in modern Pythons). [1]

For quoted display names, parseaddr returns the display name without the surrounding quotes. For example, parsing "Full Name" <foo@example.com> yields ('Full Name', 'foo@example.com'). [2]

If you need to explicitly strip surrounding quotes (or angle brackets) from an already-extracted string, use email.utils.unquote(), which removes surrounding "" or <> when present. [1]

Sources: [1] Python docs email.utils (parseaddr/unquote), [2] CPython issue showing parseaddr behavior with quoted display names.


🌐 Web query:

email.utils getaddresses comma in display names email header parsing

💡 Result:

email.utils.getaddresses() parses RFC 5322 address-lists, where commas delimit separate mailboxes (e.g., To, Cc). If a display name contains a comma, it must be a quoted-string, otherwise parsers (including getaddresses) will treat that comma as an address separator and split the value incorrectly. In RFC 5322, , is a “special” character (not allowed in an unquoted atom), and quoted-strings are the mechanism to include such characters. [2]

Correct header examples (comma in display name):

  • "Doe, John" <john@example.com>
  • "ACME, Inc." <sales@example.com>

Python behavior / what to do:

  • getaddresses([...]) is expected to split on the comma if the name isn’t quoted, because it’s parsing an address-list. [1]
  • If you’re consuming real-world, non-compliant headers and want the older, more permissive parsing behavior, Python 3.13+ added strict=; use getaddresses(fieldvalues, strict=False). [1], [3]

Best practice when generating headers: always quote/format via the stdlib (so commas get quoted properly), e.g. email.utils.formataddr((display_name, addr)). [1]

Sources: Python email.utils.getaddresses() docs (incl. strict=) [1], RFC 5322 “specials” + quoted-string rules [2], Python 3.13 “What’s New” note on strict=False/supports_strict_parsing [3].


🌐 Web query:

regex email header parsing issues quoted display names commas encoded

💡 Result:

Regex-based parsing of email header address fields (e.g., From:, To:, Cc:) breaks in exactly the cases you listed because RFC 5322 “address-list” syntax is not a regular, comma-splittable format once you include the allowed sub-structures:

  • Commas are address separators, but commas can also appear inside a quoted display-name (e.g., "Doe, John" <john@example.com>). A regex that splits on , (or uses [^,]+) will mis-split unless it correctly implements quoted-string rules. RFC 5322 also allows comments/whitespace in many places (“CFWS”), further complicating it. [1]
  • Encoded display names (RFC 2047 encoded-words) must be decoded after tokenizing/parsing the structured header, meaning “special” characters (including commas/colons) can be hidden inside encoded-words and only appear after decoding—so “decode then parse” (or regex on decoded text) can become ambiguous/unsafe. [2]
  • Real-world parsers often include lots of extra logic for these edge cases (quotes, comments, multiple addresses, robustness deviations), which is hard to reproduce reliably with a single regex. For example, .NET’s MailAddressParser explicitly handles quoted display-names and comma-separated lists with special-case logic. [3]

Practical fix: don’t use regex; use a real RFC 5322 address parser for the header, then decode RFC 2047 words in the display name after parsing. Libraries exist specifically for this (e.g., email-addresses parses RFC 5322 address lists and exposes name vs address). [4]

Test cases your solution must handle (minimum):

  • "Doe, John" <john@example.com>, Alice <alice@example.com>
  • =?UTF-8?Q?Doe=2C_John?= <john@example.com> (comma appears only after RFC 2047 decoding)
  • John (CEO) Doe <john@example.com> (comments/CFWS)

Sources: RFC 5322 address syntax and comma-separated lists [1]; encoded-word decoding occurs after structured parsing and can hide specials [2]; example of production-grade parsing logic for display-name/commas [3]; example RFC 5322 parser library designed to replace regex [4].


🌐 Web query:

Python email.utils parseaddr getaddresses RFC 2047 encoded words

💡 Result:

email.utils.parseaddr() and email.utils.getaddresses() parse RFC 5322 address syntax into (display_name, addr-spec) tuples, but they do not reliably MIME-decode RFC 2047 “encoded-word” display names for you; if a header contains =?utf-8?Q?...?= / =?utf-8?B?...?=, you typically need to decode the display-name part yourself. [1], [2], [3]

Practical pattern (decode then parse, or parse then decode names)

Option A (often simplest): decode the whole header value first, then call getaddresses:

from email.header import decode_header, make_header
from email.utils import getaddresses

raw = header_value  # e.g. msg.get('From') / msg.get('To')
decoded = str(make_header(decode_header(raw)))
pairs = getaddresses([decoded])  # [(realname, emailaddr), ...]

decode_header() splits a header into encoded/non-encoded chunks; make_header() reassembles them into a Unicode string. [3]

Option B: parse first, then decode just the display names:

from email.header import decode_header, make_header
from email.utils import getaddresses

pairs = getaddresses([raw_header_value])
pairs = [(str(make_header(decode_header(name))), addr) for name, addr in pairs]

This is the common workaround discussed for getaddresses() not decoding encoded-words automatically. [2], [3]

Important RFC 2047 edge case (malformed headers)

RFC 2047 requires encoded-words in “text” contexts to be separated from adjacent text/encoded-words by linear whitespace; some real-world mail violates this, which can break decoding unless you normalize/repair the header first. [4], [5]

Sources: Python email.utils docs (parseaddr/getaddresses) [1], Stack Overflow discussion of getaddresses() + RFC 2047 [2], Python email.header.decode_header() usage [3], RFC 2047 encoded-word rules [5], example of whitespace-separation violation [4].


Use email.utils for RFC 5322-compliant header parsing.

Regex-based parsing will mis-split headers with quoted display names containing commas (e.g., "Doe, John" <john@example.com>), a valid RFC 5322 format. This corrupts peer IDs and thread membership for imported conversations. The suggested fix correctly uses parseaddr() and getaddresses() to handle this.

However, Gmail headers may also contain RFC 2047 encoded-word display names (e.g., =?utf-8?Q?Doe=2C_John?= <john@example.com>), which parseaddr does not automatically decode. To handle both quoted commas and encoded names, decode the header first using email.header.make_header() and decode_header() before calling parseaddr:

from email.header import decode_header, make_header
from email.utils import parseaddr, getaddresses

def extract_email(from_header: str) -> str:
    """Extract bare email from 'Name <email>' format."""
    decoded = str(make_header(decode_header(from_header)))
    _, email = parseaddr(decoded)
    return email.lower().strip()

def extract_name(from_header: str) -> str:
    """Extract display name from 'Name <email>' format."""
    decoded = str(make_header(decode_header(from_header)))
    name, _ = parseaddr(decoded)
    return name.strip() or from_header.strip()

def parse_address_list(header: str) -> list[str]:
    """Parse a comma-separated email header into individual addresses."""
    if not header.strip():
        return []
    decoded = str(make_header(decode_header(header)))
    return [
        f"{name} <{addr}>" if name else addr
        for name, addr in getaddresses([decoded])
        if addr.strip()
    ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/gmail/honcho_gmail.py` around lines 123 - 132, The current
regex-based extract_email and extract_name break on valid RFC 5322 headers
(e.g., quoted commas and encoded-word names); replace the regex logic by first
decoding RFC2047 words using email.header.decode_header and make_header and then
parsing addresses with email.utils.parseaddr / getaddresses: in extract_email
decode the header, call parseaddr to get the email, lowercase and strip it; in
extract_name decode the header, call parseaddr to get the display name, strip
and fall back to the original header when empty; also add a helper like
parse_address_list that decodes the header and uses getaddresses to return
cleaned "Name <addr>" or addr entries and handle empty headers gracefully.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (4)
docs/v3/guides/gmail.mdx (1)

609-617: ⚠️ Potential issue | 🟡 Minor

Fix the Card href to use a relative path.

The href is an absolute path (/v3/documentation/core-concepts/design-patterns), which can cause 404s in Mintlify. Use a relative path from this file's location.

Suggested fix
-  <Card title="Design Patterns" icon="cubes" href="/v3/documentation/core-concepts/design-patterns">
+  <Card title="Design Patterns" icon="cubes" href="../documentation/core-concepts/design-patterns">

Based on learnings: In the Honcho documentation site (Mintlify-based), relative paths like ../../guides/... are the correct pattern for cross-references between documentation sections, not absolute paths like /v2/guides/... which can cause 404s.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/v3/guides/gmail.mdx` around lines 609 - 617, The Card linking to the
Design Patterns page uses an absolute href
"/v3/documentation/core-concepts/design-patterns" which breaks in Mintlify;
update the Card component's href (the Card inside the CardGroup) to the correct
relative path from this file (e.g., adjust the href value to a relative path
that navigates from docs/v3/guides/gmail.mdx to the design-patterns page such as
using ../../documentation/core-concepts/design-patterns or the project's correct
relative traversal) so the link resolves without 404s.
examples/gmail/honcho_gmail.py (3)

282-290: ⚠️ Potential issue | 🟡 Minor

Handle empty thread payloads in dry-run preview.

get_thread() returns {} on API errors, so fetch_thread_messages() can produce []. Accessing msgs[0] will raise IndexError.

Suggested fix
     if args.dry_run:
         print("\n[DRY RUN] Would create the above in Honcho. Showing first message per thread:")
         for tid, msgs in all_thread_messages.items():
+            if not msgs:
+                print(f"  Thread {tid}: (no messages fetched)")
+                continue
             m = msgs[0]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/gmail/honcho_gmail.py` around lines 282 - 290, In the dry-run
preview inside the args.dry_run block, avoid indexing into empty message lists
returned by fetch_thread_messages()/get_thread(): check that msgs is non-empty
before accessing msgs[0]; for empty lists (msgs == [] or falsy), either skip
that thread or print a placeholder like "No messages (failed to fetch thread)"
along with the thread id; update the loop over all_thread_messages to safely
handle and log empty payloads instead of letting msgs[0] raise IndexError.

36-48: ⚠️ Potential issue | 🟠 Major

Search beside the script, not just the current working directory.

The docstring and guide tell users to place client_secret*.json next to the script, but glob.glob() searches the current working directory. Running python examples/gmail/honcho_gmail.py from the repo root will fail even when credentials are correctly placed.

Suggested fix
-import glob
 import os
 import re
 import time
 from datetime import datetime, timezone
+from pathlib import Path
 
@@
 def find_credentials() -> str:
-    """Find a Google OAuth credentials file in the current directory."""
-    matches = glob.glob("client_secret*.json")
-    if matches:
-        return matches[0]
+    """Find a Google OAuth credentials file near the script or current directory."""
+    for directory in (Path(__file__).resolve().parent, Path.cwd()):
+        matches = sorted(directory.glob("client_secret*.json"))
+        if matches:
+            return str(matches[0])
     raise FileNotFoundError(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/gmail/honcho_gmail.py` around lines 36 - 48, The find_credentials
function currently uses glob.glob on the process current working directory which
fails when the script is run from elsewhere; modify find_credentials to search
the script's directory instead by deriving script_dir =
os.path.dirname(__file__) (or pathlib.Path(__file__).parent) and call
glob.glob(os.path.join(script_dir, "client_secret*.json")) (or
Path(script_dir).glob("client_secret*.json")) so the function (find_credentials)
finds credentials placed next to honcho_gmail.py regardless of where Python is
invoked from; return the first match or raise the existing FileNotFoundError as
before.

123-133: ⚠️ Potential issue | 🟠 Major

Use email.utils for RFC 5322-compliant header parsing.

The regex-based parsing will mis-split headers with quoted display names containing commas (e.g., "Doe, John" <john@example.com>") and won't decode RFC 2047 encoded words. This corrupts peer IDs and thread membership.

Suggested fix using stdlib
+from email.header import decode_header, make_header
+from email.utils import parseaddr, getaddresses

 def extract_email(from_header: str) -> str:
     """Extract bare email from 'Name <email>' format."""
-    match = re.search(r"<([^>]+)>", from_header)
-    return match.group(1).lower() if match else from_header.lower().strip()
+    decoded = str(make_header(decode_header(from_header)))
+    _, email = parseaddr(decoded)
+    return email.lower().strip() if email else from_header.lower().strip()


 def extract_name(from_header: str) -> str:
     """Extract display name from 'Name <email>' format."""
-    match = re.match(r'^"?([^"<]+)"?\s*<', from_header)
-    return match.group(1).strip() if match else from_header.strip()
+    decoded = str(make_header(decode_header(from_header)))
+    name, _ = parseaddr(decoded)
+    return name.strip() if name else from_header.strip()


 def parse_address_list(header: str) -> list[str]:
     """Parse a comma-separated email header into individual addresses."""
     if not header.strip():
         return []
-    parts = re.split(r",(?![^<]*>)", header)
-    return [p.strip() for p in parts if p.strip()]
+    decoded = str(make_header(decode_header(header)))
+    return [
+        f"{name} <{addr}>" if name else addr
+        for name, addr in getaddresses([decoded])
+        if addr.strip()
+    ]

Also applies to: 168-173

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/gmail/honcho_gmail.py` around lines 123 - 133, The regex parsing in
extract_email and extract_name is brittle and breaks on RFC 5322/RFC 2047 cases
(quoted names with commas, encoded-words); replace the regex logic with the
stdlib email utilities: use email.utils.parseaddr to split the header into
(name, addr) and email.header.decode_header + .join/str conversions to decode
RFC2047 encoded display names before stripping; ensure extract_email returns the
lowercased addr and extract_name returns the decoded, stripped name (and apply
the same change to the other similar functions in this file).
🧹 Nitpick comments (3)
examples/gmail/honcho_gmail.py (2)

77-77: Use explicit | None for optional parameters.

PEP 484 prohibits implicit Optional. The parameters with = None defaults should have explicit union types.

Suggested fix
-def list_threads(service, query: str = None, label_ids: list = None, max_results: int = 10) -> list[dict]:
+def list_threads(service, query: str | None = None, label_ids: list | None = None, max_results: int = 10) -> list[dict]:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/gmail/honcho_gmail.py` at line 77, The function signature for
list_threads uses parameters with default None but lacks explicit Optional
unions; update the annotation to use explicit unions (PEP 484) — e.g., change
query: str to query: str | None and label_ids: list to label_ids: list[str] |
None (keep max_results: int and return type list[dict] as-is) so the signature
of list_threads(service, query: str | None, label_ids: list[str] | None,
max_results: int = 10) -> list[dict] correctly reflects optional parameters.

333-334: Consider iterable unpacking for cleaner list construction.

This is a minor style improvement suggested by static analysis.

Suggested fix
-            msg_addresses = [m["from"]] + parse_address_list(m["to"]) + parse_address_list(m["cc"]) + parse_address_list(m["bcc"])
+            msg_addresses = [m["from"], *parse_address_list(m["to"]), *parse_address_list(m["cc"]), *parse_address_list(m["bcc"])]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/gmail/honcho_gmail.py` around lines 333 - 334, The current
construction of msg_addresses and msg_peers can be simplified by using iterable
unpacking and a single comprehension; replace the explicit list concat in
msg_addresses with unpacking (e.g., (*[m["from"]], *parse_address_list(m["to"]),
*parse_address_list(m["cc"]), *parse_address_list(m["bcc"])) or equivalent) and
build msg_peers with a single comprehension that iterates over that unpacked
iterable while calling extract_email and ensure_peer (referencing msg_addresses,
msg_peers, ensure_peer, extract_email, and parse_address_list) so the code is
more concise and removes the temporary concatenation style.
docs/v3/guides/gmail.mdx (1)

240-608: Note: This embedded script is duplicated from examples/gmail/honcho_gmail.py.

The issues flagged in the example script review (credential search path, RFC 5322 header parsing, dry-run empty thread handling) apply here as well. When fixing those issues in the example file, ensure this embedded copy is updated to match.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/v3/guides/gmail.mdx` around lines 240 - 608, This embedded script is a
duplicate of examples/gmail/honcho_gmail.py and must be updated to mirror the
fixes: update find_credentials to search common paths (current dir and parent)
and fail with a clear error message; make parse_address_list robust to RFC 5322
formats by using email.utils.getaddresses or more tolerant parsing in
parse_address_list; and avoid indexing msgs[0] in the dry-run and session
creation paths—handle empty msg lists safely (e.g., guard before accessing
msgs[0] and skip/label threads with no messages). Apply these changes to the
functions find_credentials, parse_address_list, and the dry-run/session creation
blocks so the embedded copy matches the corrected example.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@docs/v3/guides/gmail.mdx`:
- Around line 609-617: The Card linking to the Design Patterns page uses an
absolute href "/v3/documentation/core-concepts/design-patterns" which breaks in
Mintlify; update the Card component's href (the Card inside the CardGroup) to
the correct relative path from this file (e.g., adjust the href value to a
relative path that navigates from docs/v3/guides/gmail.mdx to the
design-patterns page such as using
../../documentation/core-concepts/design-patterns or the project's correct
relative traversal) so the link resolves without 404s.

In `@examples/gmail/honcho_gmail.py`:
- Around line 282-290: In the dry-run preview inside the args.dry_run block,
avoid indexing into empty message lists returned by
fetch_thread_messages()/get_thread(): check that msgs is non-empty before
accessing msgs[0]; for empty lists (msgs == [] or falsy), either skip that
thread or print a placeholder like "No messages (failed to fetch thread)" along
with the thread id; update the loop over all_thread_messages to safely handle
and log empty payloads instead of letting msgs[0] raise IndexError.
- Around line 36-48: The find_credentials function currently uses glob.glob on
the process current working directory which fails when the script is run from
elsewhere; modify find_credentials to search the script's directory instead by
deriving script_dir = os.path.dirname(__file__) (or
pathlib.Path(__file__).parent) and call glob.glob(os.path.join(script_dir,
"client_secret*.json")) (or Path(script_dir).glob("client_secret*.json")) so the
function (find_credentials) finds credentials placed next to honcho_gmail.py
regardless of where Python is invoked from; return the first match or raise the
existing FileNotFoundError as before.
- Around line 123-133: The regex parsing in extract_email and extract_name is
brittle and breaks on RFC 5322/RFC 2047 cases (quoted names with commas,
encoded-words); replace the regex logic with the stdlib email utilities: use
email.utils.parseaddr to split the header into (name, addr) and
email.header.decode_header + .join/str conversions to decode RFC2047 encoded
display names before stripping; ensure extract_email returns the lowercased addr
and extract_name returns the decoded, stripped name (and apply the same change
to the other similar functions in this file).

---

Nitpick comments:
In `@docs/v3/guides/gmail.mdx`:
- Around line 240-608: This embedded script is a duplicate of
examples/gmail/honcho_gmail.py and must be updated to mirror the fixes: update
find_credentials to search common paths (current dir and parent) and fail with a
clear error message; make parse_address_list robust to RFC 5322 formats by using
email.utils.getaddresses or more tolerant parsing in parse_address_list; and
avoid indexing msgs[0] in the dry-run and session creation paths—handle empty
msg lists safely (e.g., guard before accessing msgs[0] and skip/label threads
with no messages). Apply these changes to the functions find_credentials,
parse_address_list, and the dry-run/session creation blocks so the embedded copy
matches the corrected example.

In `@examples/gmail/honcho_gmail.py`:
- Line 77: The function signature for list_threads uses parameters with default
None but lacks explicit Optional unions; update the annotation to use explicit
unions (PEP 484) — e.g., change query: str to query: str | None and label_ids:
list to label_ids: list[str] | None (keep max_results: int and return type
list[dict] as-is) so the signature of list_threads(service, query: str | None,
label_ids: list[str] | None, max_results: int = 10) -> list[dict] correctly
reflects optional parameters.
- Around line 333-334: The current construction of msg_addresses and msg_peers
can be simplified by using iterable unpacking and a single comprehension;
replace the explicit list concat in msg_addresses with unpacking (e.g.,
(*[m["from"]], *parse_address_list(m["to"]), *parse_address_list(m["cc"]),
*parse_address_list(m["bcc"])) or equivalent) and build msg_peers with a single
comprehension that iterates over that unpacked iterable while calling
extract_email and ensure_peer (referencing msg_addresses, msg_peers,
ensure_peer, extract_email, and parse_address_list) so the code is more concise
and removes the temporary concatenation style.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5b19d8ea-9293-435e-a582-677736f345b3

📥 Commits

Reviewing files that changed from the base of the PR and between 13497ed and 4080842.

📒 Files selected for processing (4)
  • docs/v3/documentation/core-concepts/design-patterns.mdx
  • docs/v3/guides/gmail.mdx
  • docs/v3/guides/granola.mdx
  • examples/gmail/honcho_gmail.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/v3/guides/granola.mdx

**Peer join order matters**

Peers don't have to be added to a session all at once. You can call `session.add_peers()` multiple times as participants appear — and Honcho will take join order into account when reasoning. A peer only has context from the point they joined, not from earlier in the session.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A Peer will only "reason" over data it sees or directly observes happen, if it joins a session after the fact it won't then schedule all of the previous messages to be reasoned against

@ajspig ajspig marked this pull request as draft March 18, 2026 17:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants