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
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
90 lines
5.6 KiB
Markdown
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.
|