DEV Community

Andy Khang
Andy Khang

Posted on

Sloth Digest 🦥: Private Overnight Gmail Briefing & Reply-Debt Tracker with Gemma, Temporal & Mastra

Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady Submission 🦥

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady

What I Built

In honor of International Sloth Day and the "Slow and Steady" theme, I built Sloth Digest 🦥 — a 100% private, local-first overnight Gmail executive briefing and Reply-Debt Tracker that does the dreaded morning inbox catch-up while you sleep.

Waking up to 40 unread emails, sifting through newsletters, and anxiously wondering "Wait, did I ever reply to Sarah's question from Tuesday?" is the opposite of sloth energy. Instead of handing your most sensitive personal data to a cloud LLM API, Sloth Digest runs entirely on your home server or laptop:

  1. Sleep through the night: At 6:00 AM, a durable Temporal schedule wakes up on your machine.
  2. Read-only IMAP ingestion: It inspects new messages in your Gmail INBOX and [Gmail]/Sent Mail using strict RFC 3501 EXAMINE mode with UID cursors — so it never marks unread emails as read or mutates your mailbox.
  3. Local Gemma inference via llama.cpp: An open-weight Gemma model served locally over llama.cpp classifies mail (personal, transactional, bulk), summarizes multi-message personal threads into strictly two lines, and extracts unanswered questions directed at you.
  4. Auto-Reconciled Reply Debt: Every direct ask is recorded in a local SQLite reply_debt ledger (NEW, PENDING 3d, etc.). Whenever you reply from any email client during the day, the next run inspects your Sent folder's In-Reply-To and thread headers and automatically resolves the debt.
  5. Wake-up delivery: At 7:00 AM, a self-hosted ntfy push notification lands on your phone with your reading time saved and pending asks, linked to a zero-dependency LAN web dashboard (http://localhost:3000).

Demo, Prerequisites & Quickstart

Before Starting the App (Live Gmail + Full Stack Prerequisites)

Before launching Sloth Digest against your real Gmail inbox, prepare the following prerequisites:

  1. Runtime Environment:
    • Node.js 22+ and npm (for local CLI execution)
    • Docker & Docker Compose (recommended for running the full 4-container stack: sloth-llama, sloth-temporal, sloth-ntfy, and sloth-app)
  2. Google App Password for Gmail (imap.gmail.com:993):
    • Gmail disallows standard account passwords over IMAP. First, ensure 2-Step Verification (2FA) is enabled on your Google Account (https://myaccount.google.com/security).
    • Generate a 16-character Google App Password at https://myaccount.google.com/apppasswords and copy the 16-character code.
    • Verify that IMAP access is enabled in your Gmail forwarding/POP/IMAP settings.
  3. Quantized Gemma GGUF Weights (./models/):
    • Create the local ./models directory (mkdir -p models) and download your quantized Gemma GGUF model file into it (for example, gemma-2-2b-it-Q4_K_M.gguf or a Gemma 4B GGUF model matching LLAMA_MODEL_FILE in .env).
  4. Environment Variables (.env):
    • Copy cp .env.example .env and configure your credentials and schedule before starting the app:
# Gmail IMAP Credentials (requires 2FA + 16-char Google App Password)
GMAIL_USER=your-email@gmail.com
GMAIL_APP_PASSWORD=your-16-char-app-password
# Or mount via Docker secret file:
# GMAIL_APP_PASSWORD_FILE=/run/secrets/gmail_password

# llama.cpp Server (OpenAI-compatible /v1 endpoint running Gemma)
LLAMA_SERVER_URL=http://localhost:8080
LLAMA_MODEL_NAME=gemma-2-2b-it
LLAMA_MODEL_FILE=gemma-2-2b-it-Q4_K_M.gguf

# Temporal Schedule & Timezone
TEMPORAL_ADDRESS=localhost:7233
DIGEST_SCHEDULE_HOUR=6
DIGEST_SCHEDULE_MINUTE=0
DIGEST_SCHEDULE_TIMEZONE=Asia/Ho_Chi_Minh

# Self-Hosted ntfy Push Delivery & LAN Dashboard
NTFY_URL=http://localhost:8088/sloth-digest
NTFY_SCHEDULE_TIME=07:00
WEB_PORT=3000
# Optional: Protect the LAN dashboard with HTTP Basic / Bearer auth
# WEB_AUTH_TOKEN=your-secret-dashboard-token
Enter fullscreen mode Exit fullscreen mode

Option A: Start the Full Self-Hosted Stack (Docker Compose)

Once .env and ./models/ are ready, bring up llama-server, Temporal (with Web UI on 127.0.0.1:8233), ntfy (127.0.0.1:8088), and the unified sloth-app worker + dashboard (http://localhost:3000):

# Build and start all 4 containers
docker compose up -d --build

# Register the 06:00 AM daily Temporal schedule
docker exec -it sloth-app npx tsx src/cli.ts schedule

# Or trigger an immediate digest run right now
docker exec -it sloth-app npx tsx src/cli.ts run-now
Enter fullscreen mode Exit fullscreen mode

Option B: Instant Offline Demo (Zero Credentials Needed for Judges)

Want to test the entire pipeline immediately without setting up a Google App Password or downloading GGUF weights first? Use the built-in synthetic inbox seeder and deterministic fallback:

# 1. Clone and install
git clone https://github.com/AndyKhang404/sloth-digest.git
cd sloth-digest
npm install
cp .env.example .env

# 2. Run the automated verification suite (12/12 assertions)
npm run test-pipeline

# 3. Seed realistic multi-thread emails + a Sent reply into local SQLite
npm run seed -- --local

# 4. Run the standalone local digest pipeline
npm run sloth -- run-local

# 5. Inspect active reply debt in the CLI or launch the LAN web dashboard
npm run sloth -- debt
npm run sloth -- serve
Enter fullscreen mode Exit fullscreen mode

Sample CLI & Digest Output

When the pipeline finishes, it writes a clean Markdown briefing to ./data/digests/ and serves it at http://localhost:3000:

# 🦥 Sloth Digest — Executive Morning Briefing
** Lazy Payoff:** Saved **14.2 min** of reading (3,650 words condensed to 270 words @ 238 WPM)

## 🌅 Two-Line Executive Overview
- Processed 3 active personal threads overnight while skipping 4 bulk/transactional messages.
- You have 2 open reply-debt items awaiting your response today (1 thread auto-resolved via Sent mail).

## 🔴 Active Reply Debt (Needs Your Reply)
1. **[NEW] Production Migration Window Confirmation** — from `sarah.chen@example.com`
   - **Ask:** Can you confirm if Sunday 02:00 UTC works for the database cutover?
   - **Cited Message-ID:** `<msg-prod-cutover-02@example.com>`
2. **[PENDING 2d] Q4 Architecture Sign-off** — from `alex.rivera@example.com`
   - **Ask:** Please review the updated latency numbers in section 3 before Friday.
   - **Cited Message-ID:** `<msg-q4-arch-01@example.com>`
Enter fullscreen mode Exit fullscreen mode

Code

GitHub logo AndyKhang404 / sloth-digest

Private overnight Gmail digest and reply-debt tracker powered locally by Gemma, llama.cpp, Mastra, and Temporal.

🦥 Sloth Digest

Private Overnight Gmail Executive Briefing & Reply-Debt Tracker
Built for Hacktoberfest 2026 Week 3: "Slow and Steady"
Lead Category: Best Use of Gemma ($200) • Secondary: Best Use of Temporal, Best Use of Mastra

License: MIT Gemma llama.cpp Temporal Mastra


📖 What is Sloth Digest?

In honor of International Sloth Day and the Hacktoberfest 2026 "Slow and Steady" theme, Sloth Digest channels your inner sloth by doing the dreaded morning email catch-up while you sleep.

Instead of waking up to 40 unread emails, scrolling through noise, and dreading forgotten replies:

  1. Sleep through the night: At 6:00 AM, a durable Temporal workflow wakes up on your home server.
  2. Read-only triage: It examines new emails in your Gmail inbox without modifying read flags or leaving your local network.
  3. Local AI synthesis: An open-weight Gemma 4B model running on llama.cpp categorizes bulk mail, summarizes personal threads into strictly two lines, and extracts direct asks.
  4. Durable…

How I Built It

Architecture

flowchart TD
    Gmail["Gmail IMAP (Read-Only EXAMINE)"] -->|UID Cursor Fetch| Temporal["Temporal Workflow (nightlyEmailDigestWorkflow)"]
    Temporal --> Ingest["1. fetchNewEmailsActivity (INBOX + Sent)"]
    Temporal --> Classify["2. classifyEmailsActivity (Gemma + Mastra)"]
    Temporal --> Summarize["3. summarizeThreadActivity (2-Line Thread Summary)"]
    Temporal --> Reduce["4. reduceAndGenerateDigestActivity (Reply-Debt Engine + SQLite)"]
    Temporal --> Notify["5. deliverNotificationActivity"]
    Classify <--> Llama["llama.cpp Server (Gemma GGUF on 127.0.0.1:8080)"]
    Summarize <--> Llama
    Reduce --> Dashboard["LAN Web Dashboard (:3000)"]
    Notify --> Ntfy["Self-Hosted ntfy Push (:8088)\nScheduled 07:00 Delivery"]

Joining the Community Conversation on Local AI & Email Agents

While designing Sloth Digest, three discussions from the DEV community directly shaped the architecture:

  • Zero-rewrite local inference with llama.cpp: In How to serve an OpenAI-compatible local API with llama.cpp by @dedupedai, the author demonstrates exposing a local GGUF model over /v1/chat/completions with JSON schema constraints. We used this exact pattern so our Mastra agents talk to llama-server on 127.0.0.1:8080 with response_format: { type: 'json_object' }.
  • Deterministic verifiers around small local models: In Beyond Vibes: Architecting Closed-Loop AI Agents with Local SLMs, Deterministic Evals, and Human Learning Loops by @akshatsoni26, the core takeaway is "Numbers owned by code, words owned by models" — never trust a small local model to invent IDs or calculate metrics unverified. Inspired by that guardrail pattern, Sloth Digest cross-checks every cited_message_ids entry returned by Gemma against a deterministic Set of real RFC 822 Message-IDs in that thread, and computes reading-time savings in pure TypeScript.
  • Why email is tough for agents: In Giving Autonomous Agents Access to Email by @francofuji, the author highlights how traditional email polling and state management trip up autonomous agents. For personal inboxes, the biggest hazard is side effects (accidentally marking unread messages as \Seen or choking on huge attachments), which drove our strict read-only EXAMINE + 100 KB source cap design.

Key Implementation Details

1. Strictly Read-Only IMAP with UID Cursors (src/ingest/imap.ts)

If an overnight script marks your emails as "Read" before you wake up, you will miss critical messages when you open your phone. We open both INBOX and [Gmail]/Sent Mail with { readOnly: true } (IMAP EXAMINE) and track (uidvalidity, last_uid) cursors in SQLite:

const inboxLock = await imap.getMailboxLock('INBOX', { readOnly: true });
try {
  const mailbox = imap.mailbox;
  if (mailbox && mailbox.exists > 0) {
    const uidvalidity = String(mailbox.uidValidity);
    const cursor = dbService.getCursor(accountId, 'INBOX');
    const lastUid = cursor?.uidvalidity === uidvalidity ? cursor.last_uid : 0;
    // Fetch only UIDs > lastUid with a 100 KB source cap to skip heavy attachments
  }
} finally {
  inboxLock.release();
}
Enter fullscreen mode Exit fullscreen mode

2. Prompt-Injection Isolation & Citation Verification (src/agent/mastra.ts)

Incoming emails are untrusted external input. Before passing any email into our Mastra agents (EmailTriageAgent, ThreadSummarizerAgent, DigestOverviewAgent), we strip scripts, styles, hidden zero-width/bidirectional Unicode characters, and wrap the text inside <untrusted_email_data> tags with boundary neutralization. Then we validate the output with zod and filter any hallucinated Message-ID citations:

export const ThreadSummarySchema = z.object({
  thread_summary: z.tuple([z.string(), z.string()]),
  needs_reply: z.boolean(),
  reply_reason: z.string().optional(),
  cited_message_ids: z.array(z.string()),
});

// Deterministic validation: filter out any cited message IDs not actually in this thread
const validMessageIds = new Set(messages.map((m) => m.message_id));
const validatedCitations = result.cited_message_ids.filter((id) => validMessageIds.has(id));
Enter fullscreen mode Exit fullscreen mode

3. Auto-Resolving Reply Debt via Sent Mail (src/engine/debt.ts)

Instead of forcing you to manually check off to-do items every day, reconcileSentReplies checks if you replied to the thread or In-Reply-To message ID from your phone or desktop client, and clears the debt automatically:

const normalizeMsgId = (id: string) => id.trim().replace(/^<+|>+$/g, '');

for (const reply of sentReplies) {
  const normalizedReplyTarget = reply.inReplyTo ? normalizeMsgId(reply.inReplyTo) : '';
  for (const debt of openDebts) {
    if (debt.status !== 'open') continue;
    const matchesInReplyTo =
      Boolean(normalizedReplyTarget) &&
      normalizedReplyTarget === normalizeMsgId(debt.message_id);
    const matchesThread = reply.threadId === debt.thread_id;

    if (matchesInReplyTo || matchesThread) {
      dbService.resolveReplyDebtByThread(debt.thread_id, reply.messageId);
      debt.status = 'resolved';
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Durable Orchestration with Temporal (src/temporal/workflows.ts)

Home servers reboot, Wi-Fi blips, and CPU inference can spike. Wrapping the 5 pipeline stages in a Temporal workflow (nightlyEmailDigestWorkflow) with activity retries, exponential backoff, and heartbeats ensures the 6:00 AM digest finishes reliably before the 7:00 AM push notification fires.

What I Would Do Differently Next

Right now, thread summarization runs sequentially per active thread to keep CPU and RAM usage gentle on low-power home servers. Next, I would add configurable concurrency limits in the Temporal worker alongside a lightweight local embedding index so follow-up threads from weeks ago can link prior context automatically.


Why Does Open Innovation Matter?

Your personal email inbox holds bank alerts, medical updates, private family conversations, and work contracts. Sending every unread email to a proprietary cloud LLM API every night means trading away your privacy and paying recurring token fees forever.

Open innovation makes Sloth Digest possible on three fronts:

  1. Privacy by Architecture (Open-Weight Gemma + llama.cpp): Because Gemma runs locally as a quantized GGUF model bound strictly to 127.0.0.1:8080, zero bytes of email content ever leave your LAN.
  2. Sloth Economics ($0/month Forever): Nightly batch triage across dozens of emails costs $0.00 in API fees. Once your container stack is up, it runs quietly on existing hardware.
  3. Verifiable, Self-Hosted Stack: Every component — from Mastra agent schemas and Temporal workflow orchestration to ntfy push notifications and SQLite — runs in rootless local Docker containers that you can inspect, audit, and customize.

The Lazy Payoff

Sloth Digest calculates your exact time saved on every run using the standard human reading speed benchmark of 238 words per minute:

Metric Cold Inbox Morning With Sloth Digest 🦥
Morning Triage Time 30–60 minutes scanning threads & promos ~2 minutes reading a 2-line-per-thread briefing
Forgotten Replies Searching "did I reply to that?" days later Auto-tracked Reply Debt cleared when you hit Send
Cloud AI Exposure 100% of inbox sent to third-party servers 0 bytes leave your machine

Over a year, saving just 25 minutes each morning reclaims ~150 hours of focus — all while you sleep in.


Prize Categories

This project targets the following Hacktoberfest Week 3 partner categories:

  • Best Use of Gemma: Google's open-weight Gemma model (served locally via llama.cpp) powers all three stages of intelligence: inbox triage (classifyEmailWithGemma), strict two-line thread summarization + ask extraction (summarizeThreadWithGemma), and the morning executive briefing (generateOverallOverviewWithGemma).
  • Best Use of Temporal: nightlyEmailDigestWorkflow orchestrates the 5-step ingestion, classification, thread map-reduce summarization, SQLite ledger reconciliation, and notification delivery pipeline with activity timeouts, heartbeats, exponential backoff retries, and a 6:00 AM cron schedule.
  • Best Use of Mastra: @mastra/core defines our structured local agents (EmailTriageAgent, ThreadSummarizerAgent, DigestOverviewAgent) paired with strict zod schema validation (EmailCategorySchema, ThreadSummarySchema, InboxOverviewSchema) and prompt-injection boundary sanitization.

Top comments (0)