PSA/ee/docs/plans/2026-03-31-inbound-email-reopen-on-reply-design.md
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

90 lines
5.6 KiB
Markdown

# Inbound Email Reopen On Reply Design
- Date: `2026-03-31`
- Status: `Approved`
## Summary
Improve inbound email threading against closed tickets by adding ConnectWise-style reopen behavior with per-board configuration, a configurable cutoff window, and optional AI suppression for simple client acknowledgements. Reopen-on-reply remains core behavior; AI suppression is an Enterprise Edition enhancement gated by the `AI Assistant` add-on.
## Current State
Inbound replies are matched in [`shared/services/email/processInboundEmailInApp.ts`](../../shared/services/email/processInboundEmailInApp.ts) via reply token or thread headers and then appended as comments. The matched-ticket reply path does not currently perform any inbound-specific reopen transition for closed tickets.
Ticket statuses are board-owned. Boards already have exactly one open default status, and existing ticket update paths already align `status_id`, `is_closed`, `closed_at`, and `closed_by` when moving between open and closed statuses.
The current inbound heuristic only filters token-only or effectively empty replies by stripping Alga reply markers and checking whether any text remains.
## Product Decisions
- Reopen behavior is configured per board.
- Internal user replies always reopen closed tickets when threading matches and the cutoff window has not expired.
- Client/contact replies reopen closed tickets when threading matches and the cutoff window has not expired, unless AI suppression is enabled and classifies the reply as a simple acknowledgement.
- Boards may define an explicit `reopen_status_id`.
- If `reopen_status_id` is unset, reopening falls back to the board's default open status.
- Boards define a configurable cutoff window. Replies beyond that window do not revive the old ticket and instead enter the new-ticket path.
- AI acknowledgement suppression is a configurable board option so users can discover and opt out of it.
## Board Configuration
Each board gains reopen-on-reply policy fields:
- `reopen_on_inbound_reply_enabled`
- `reopen_cutoff_minutes` or equivalent duration field
- `reopen_status_id` nullable
- `ai_ack_suppression_enabled`
These settings belong to boards rather than providers because reopen semantics and status ownership are board-scoped.
## Runtime Decision Flow
When inbound email matches an existing ticket:
1. Load the matched ticket, board, and current status.
2. If the ticket is not closed, keep current behavior.
3. If the ticket is closed and the board policy disables reopen-on-reply, add the comment without reopening.
4. If the ticket is closed and the cutoff window is exceeded, do not attach to the old ticket; route into the existing new-ticket path.
5. If the sender is internal, reopen immediately.
6. If the sender is a client/contact:
- Run the existing cheap heuristic first.
- If AI suppression is disabled, or the tenant lacks `AI Assistant`, or the reply is clearly substantive, reopen normally.
- If AI suppression is enabled and the reply is short and plausibly acknowledgement-like, call the standard LLM path with a tightly constrained prompt and require a tiny formatted response such as `ACK` or `NOT_ACK`.
- `ACK` keeps the ticket closed and still records the comment.
- `NOT_ACK` reopens normally.
7. Reopen by updating `status_id`, `is_closed`, `closed_at`, and `closed_by` using the same transition semantics used in existing ticket update paths.
## AI Architecture
Reopen-on-reply is core behavior, but AI suppression is an EE enhancement and should follow the existing CE/EE split:
- Shared/server code depends on a small pluggable interface, for example `InboundReplyAcknowledgementDecider`.
- The default CE implementation performs no semantic suppression and always returns "not acknowledgement".
- In EE mode, shared/server code manually resolves an EE implementation from `@ee/...`.
- The EE implementation may call the standard chat-completions provider already used elsewhere in the product.
- The EE implementation must still require the `AI Assistant` add-on and the board-level `ai_ack_suppression_enabled` flag before issuing any LLM call.
- If EE code is unavailable, entitlement fails, or the LLM call errors, the system falls back to normal reopen behavior.
This keeps AI-specific prompt and model behavior isolated in EE code while leaving the core reopen flow shared.
## Failure Behavior
- If no valid reopen target can be resolved, attach the comment without reopening and log the configuration issue.
- If AI suppression cannot run for any reason, reopen normally rather than blocking email processing.
- Existing dedupe must continue to run before reopen/comment work so duplicate deliveries do not cause repeated reopen transitions.
- The system should record decision metadata sufficient to explain outcomes later, such as reopen reason, cutoff result, and AI suppression result.
## Test Coverage
Prefer behavioral integration coverage around the inbound email flow:
- Closed ticket + internal reply reopens to explicit reopen status.
- Closed ticket + internal reply without explicit reopen status reopens to board default open status.
- Closed ticket + client reply with AI suppression disabled reopens.
- Closed ticket + client reply with AI suppression enabled and `ACK` stays closed while adding the comment.
- Closed ticket + client reply with AI suppression enabled and `NOT_ACK` reopens.
- Closed ticket + client reply with AI enabled but no `AI Assistant` add-on reopens without AI.
- Closed ticket + AI failure reopens without AI.
- Closed ticket + reply beyond cutoff creates a new ticket instead of attaching to the closed one.
- Open ticket replies preserve current behavior.
- Token-only replies remain skipped by the existing heuristic.