Skip to content

feat([issue-2796]): extend Tribe unanswered-thread detection to Gmail via per-account sent ingestion#2821

Merged
atomantic merged 5 commits into
mainfrom
next/issue-2796
Jul 20, 2026
Merged

feat([issue-2796]): extend Tribe unanswered-thread detection to Gmail via per-account sent ingestion#2821
atomantic merged 5 commits into
mainfrom
next/issue-2796

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

Extends Tribe-outreach unanswered-thread detection (#2158) to Gmail. Previously it was gated to iMessage/Signal because email sync ingested the inbox only — a Gmail reply never produced a message.sent timeline event, so every stale inbound read as unanswered forever.

  • Sent-mail ingestion (Gmail): the API sync now fetches recent sent mail (in:sent newer_than:14d) as a separate, capped list pass and records it into the human-activity timeline as message.sent events. Sent mail is activity-only — it never enters the inbox cache/triage/trim, so it can't pollute /api/messages/inbox or evict real inbox mail. Gated per-account via syncConfig.ingestSent (default-on for Gmail; opt out with a new "Reply detection" toggle in Messages > Config).
  • Per-account two-way gate (not source-wide): buildTwoWayGate resolves which accounts are trustworthy and filters inbound/sent events by accountId. A source-wide gate would let one Gmail account's sent import vouch for every Gmail account. An email account is trusted only when it's enabled, has an owner email, opts in, and has a recent sent-ingest watermark (sentIngestedAt) — so an account default-on at upgrade or with failing OAuth/sync is never trusted before reply evidence actually exists (closing the false-nudge window).
  • Correct sender resolution: the shared email mapper now sets metadata.handle to the sender's email on received mail, so enrichActivityEvent can tag the Tribe sender (without it, every Gmail inbound was dropped).
  • Authoritative direction: direction honors Gmail's SENT label, so a reply sent from a send-as alias is classified as sent and cancels its inbound.
  • Channel-appropriate drafts: Gmail outreach drafts use an email-toned template (greeting + sign-off) rather than the casual no-signoff chat template.

Recipient/draft grounding via threadId was already supported and is reused unchanged.

Follow-on robustness (per-account timeline event querying, full sent-window pagination, optional legacy-row backfill) is filed as #2820.

Test plan

  • server/services/tribeOutreach.test.js — per-account gate: chat-source-wide, Gmail default-on, opt-out, missing owner email, disabled account, missing/stale watermark, no cross-account vouching, Outlook never two-way; plus email-vs-chat template selection.
  • server/services/messageGmailSync.test.js — inbox/sent query builders, separate sent-pass budget, and the SENT_INGEST_DAYS >= DEFAULT_WITHIN_DAYS coupling guard.
  • server/services/humanActivity.test.js — received mail sets metadata.handle to the sender; SENT-label direction classifies an alias-sent reply as message.sent.
  • server/services/messageSync.test.js — updated for the new markSentIngested export; sent mail recorded as activity without entering the cache.
  • Full affected suite: 172 passing. Reviewed locally with claude + ollama + codex (series); all findings fixed or filed (Tribe outreach (Gmail): per-account event querying, full sent-window coverage, and legacy-row backfill #2820).

Closes #2796

… via per-account sent ingestion

Email sync ingested the inbox only, so a Gmail reply never reached the
human-activity timeline and every stale inbound read as unanswered forever —
detection was gated to iMessage/Signal.

Gmail's API sync now folds recent sent mail (in:sent newer_than:14d) into the
same cache the timeline ingest reads, producing message.sent events so the
existing lastSentT >= inbound answered-check works per email thread. Ingestion
is a per-account capability (syncConfig.ingestSent, default-on for Gmail,
opt-out via a new ConfigTab toggle).

The outreach detector's gate is now PER ACCOUNT, not source-wide: a source-wide
gate would let one Gmail account's sent import vouch for every Gmail account
(including ones with no sent evidence). buildTwoWayGate resolves trustworthy
accounts from message-account config and filters inbound/sent events by
accountId. Outlook/Teams stay one-way until a sent-fetch path exists.
…sent mail its own fetch budget

- CRITICAL: the shared email mapper set no metadata.handle, so enrichActivityEvent
  couldn't resolve the sender's personId and every Gmail inbound was dropped by the
  outreach detector — the feature was non-functional for email. Set metadata.handle
  to the sender email on received mail (mirrors the iMessage mapper).
- MEDIUM: inbox+sent were OR-combined under one shared cap, so a heavy sender's sent
  mail could evict unread inbox from the primary sync. Fetch sent as a separate list
  pass with its own SENT_INGEST_MAX budget (gmailSyncPasses).
- LOW: pin SENT_INGEST_DAYS >= DEFAULT_WITHIN_DAYS with a test; note the send-as-alias
  direction-detection limitation in a comment.
- Tests: mapper handle contract, sync-pass budgets, constant coupling.
…k, alias direction, email draft tone

Fixes for 6 P1 findings from the codex review pass:
- Sent mail no longer enters the inbox cache/triage/trim — syncGmail returns it
  separately and messageSync records it as timeline activity only (was polluting
  /api/messages/inbox and could be trimmed before its reply evidence was recorded).
- The two-way gate now requires an enabled account with a RECENT sent-ingest
  watermark (sentIngestedAt, stamped on successful Gmail sync), so an account
  default-on at upgrade or with failing OAuth/sync is not trusted before reply
  evidence exists — closing the false-nudge window.
- Direction now honors Gmail's authoritative SENT label, so a reply sent from a
  send-as alias is classified as sent and cancels its inbound.
- Gmail outreach drafts use an email-toned template (greeting + sign-off) instead
  of the casual no-signoff chat template.
- Raised the sent fetch budget to 100 (crowding gone now that sent is activity-only).
- Tests: watermark/enabled/stale gate cases, SENT-label alias direction, template
  selection, messageSync mock updated for the new markSentIngested export.

Deferred robustness (per-account event querying, full sent-window coverage,
legacy-row backfill) filed as #2820.
@atomantic
atomantic merged commit 472d988 into main Jul 20, 2026
2 checks passed
@atomantic
atomantic deleted the next/issue-2796 branch July 20, 2026 02:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tribe outreach: extend unanswered-thread detection to email (needs sent/replied ingestion)

1 participant