Files
broccolini-bot/.scratch/folder-lifecycle/design.md

3.5 KiB

Folder lifecycle: Triage → Awaiting Reply → Needs Response

Goal

Auto-advance a ticket's Gmail folder as the conversation moves, so the inbox reflects who owes a reply:

  • New email ticket → Triage (unchanged).
  • Staff reply reaches the customer → Awaiting Reply.
  • Customer responds → Needs Response.
  • Escalate → Escalated (unchanged; cycle resumes on the next reply/response).
  • Close → Resolved (unchanged).

Triage means "new, not yet replied to"; the first staff reply moves Triage→Awaiting Reply.

Folder taxonomy (new classification)

  • Auto-cycle (bot moves these): TRIAGE, ESCALATED, AWAITING_REPLY, NEEDS_RESPONSE, RESOLVED.
  • Manual / exempt (bot never auto-moves): FOR_JAKE, SPAM, PARTNERSHIP_OFFERS, DASHBOARD_ERRORS.

A customer reply (or staff reply) to a thread sitting in a manual folder leaves it there; only auto-cycle (or unlabeled) threads advance.

Mechanism

New services/gmailLabels.js exports:

  • MANUAL_KEYS — the exempt set above.
  • getManagedFolderKey(threadId, gmail) — reads the thread's existing labels (union of message labelIds), maps to a managed folder key, returns it or null. Read-only: never creates a label just to check (uses the existing name→id cache; SPAM maps to the system SPAM id).
  • autoAdvanceFolder(threadId, targetKey, gmail):
    current = getManagedFolderKey(threadId, gmail)
    if current ∈ MANUAL_KEYS: return false      // filed manually → don't touch
    await moveThreadToFolder(threadId, targetKey, gmail)
    return true
    

Decisions baked in (from brainstorming)

  • "A response given" = a reply that actually emails the customer. Today that's the typed-reply path (handleDiscordReplysendGmailReply). /response send does NOT email today, so it is intentionally NOT wired here; if it's ever wired to email, it routes through the same send point and gets the move for free.
  • Escalation keeps its own distinct ESCALATED folder; it is auto-cycle eligible, so the next staff reply → Awaiting Reply and the next customer reply → Needs Response.
  • Manual filings (For Jake / Spam / Partnership / Dashboard Errors) are never overridden by the auto-flow.

Hooks

  • handlers/messages.js — after a successful sendGmailReply, fire-and-forget autoAdvanceFolder(ticket.gmailThreadId, 'AWAITING_REPLY').
  • gmail-poll.js (~line 308, the follow-up-to-existing-channel branch) — replace the "leave folder untouched" behavior with fire-and-forget autoAdvanceFolder(parsed.threadId, 'NEEDS_RESPONSE', gmail).

Both guard Discord-origin tickets (gmailThreadId starts discord-) and are fire-and-forget (.catch(() => {})) so a label failure (e.g. stale Gmail auth) never breaks the reply or the poll.

Files

  • services/gmailLabels.js — 2 new FOLDER_DEFS entries; MANUAL_KEYS; getManagedFolderKey(); autoAdvanceFolder(); add to exports.
  • config.jsGMAIL_LABEL_AWAITING_REPLY (default Awaiting Reply), GMAIL_LABEL_NEEDS_RESPONSE (default Needs Response).
  • .env.example — document the two new vars.
  • handlers/messages.js — Awaiting Reply hook.
  • gmail-poll.js — Needs Response hook.
  • tests/gmailLabels.test.js — cover manual-exemption gating + happy-path advance.

Out of scope

  • Wiring /response send to email (separate concern; user is unsure /response is even worth keeping).
  • Reopen flow (a customer reply after the Discord channel is deleted creates a new channel → Triage via the existing path; unchanged).