74 lines
3.5 KiB
Markdown
74 lines
3.5 KiB
Markdown
# 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 (`handleDiscordReply` → `sendGmailReply`). `/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.js` — `GMAIL_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).
|