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
20 KiB
20 KiB
Scratchpad — Inbound Email Sender Routing to Boards
- Plan slug:
inbound-email-sender-routing-to-boards - Created:
2026-02-25
What This Is
Keep a lightweight, continuously-updated log of discoveries and decisions made while implementing this plan.
Prefer short bullets. Append new entries as you learn things, and also update earlier notes when a decision changes or an open question is resolved.
Decisions
- (2026-02-25) Revised plan ownership from provider-level sender-rule table to client/contact-owned routing configuration.
- (2026-02-25) Destination target stays as
inbound_ticket_defaults_id(profile-level), not board-only override. - (2026-02-25) Precedence for new-ticket creation: contact override -> client destination -> provider default.
- (2026-02-25) Existing-ticket reply flows remain unchanged; no rerouting for threaded replies.
- (2026-02-25)
F001completed as a plan-model checkpoint: implementation work proceeds only with client/contact-owned routing and does not introduce provider-level sender-rule tables. - (2026-02-25) Shared resolver
resolveEffectiveInboundTicketDefaultsis the single precedence source for contact override / contact-client default / provider fallback used by both in-app and runtime context paths. - (2026-02-25) Domain-matched client routing now contributes to destination resolution via the same shared resolver (
client_default_from_domainbefore provider fallback). - (2026-02-25) Destination IDs from contacts/clients are runtime-validated against
inbound_ticket_defaultswith(tenant, id, is_active=true)before use; invalid/inactive mappings now warn and fallback.
Discoveries / Constraints
- (2026-02-25) Existing sender-domain-to-client mapping already exists and is explicit via
client_inbound_email_domains. - (2026-02-25)
processInboundEmailInAppcurrently sets ticket board from provider defaults (defaults.board_id) and does not vary by sender. - (2026-02-25)
resolve_inbound_ticket_contextcurrently resolves target client/contact/location only; it does not yet compute sender-based destination defaults. - (2026-02-25)
ClientDetailsalready includes inbound-domain and default-contact controls, making client-level destination a natural extension. - (2026-02-25)
clientscurrently had no dedicated inbound destination field; persisted client destination now starts with nullableclients.inbound_ticket_defaults_id. - (2026-02-25)
contactscurrently had no dedicated inbound destination override; persisted contact override now starts with nullablecontacts.inbound_ticket_defaults_id. - (2026-02-25) Added explicit tenant-scoped indexes for lookup safety/perf:
idx_clients_tenant_inbound_ticket_defaultsidx_contacts_tenant_inbound_ticket_defaults
- (2026-02-25) Local worktree currently has no installed npm dependencies (
npm ls vitest --depth=0is empty), so targeted Vitest runs fail at startup before test execution.
Commands / Runbooks
- (2026-02-25) Read current inbound processing and context resolution:
rg -n "processInboundEmailInApp|resolve_inbound_ticket_context|resolveInboundTicketDefaults" shared/services/email shared/workflow
- (2026-02-25) Read existing domain-match plans and tests:
ee/docs/plans/2026-02-13-inbound-email-domain-matching-default-contact/server/src/test/integration/inboundEmailInApp.webhooks.integration.test.tsserver/src/test/integration/resolveInboundTicketContext.domainFallback.integration.test.ts
- (2026-02-25) Scaffold and validate plan:
python3 /Users/roberisaacs/.codex/skills/alga-plan/scripts/scaffold_plan.py "Inbound Email Sender Routing to Boards" --slug inbound-email-sender-routing-to-boardspython3 /Users/roberisaacs/.codex/skills/alga-plan/scripts/validate_plan.py ee/docs/plans/2026-02-25-inbound-email-sender-routing-to-boards
- (2026-02-25) Add client destination schema migration:
server/migrations/20260225120000_add_client_inbound_ticket_defaults_id.cjs
- (2026-02-25) Add contact override schema migration:
server/migrations/20260225120500_add_contact_inbound_ticket_defaults_id.cjs
- (2026-02-25) Add lookup index migration:
server/migrations/20260225121000_add_inbound_ticket_defaults_lookup_indexes.cjs
- (2026-02-25) Targeted unit tests attempted for shared/runtime email routing changes:
npx vitest run shared/services/email/__tests__/processInboundEmailInApp.test.ts shared/services/email/__tests__/processInboundEmailInApp.additionalPaths.test.ts shared/workflow/runtime/actions/__tests__/registerEmailWorkflowActions.contactAuthorship.test.ts- Blocker in local env: Vitest startup fails because
dotenvpackage is missing from active node_modules resolution path.
- (2026-02-25) Targeted migration integration test attempted:
cd server && npx vitest run src/test/integration/inboundTicketDestinationMigrations.integration.test.ts --reporter=dot- Blocker in local env: Vitest startup fails because
vitestpackage is not installed in this worktree.
- (2026-02-25) Targeted in-app routing integration test attempted:
cd server && npx vitest run src/test/integration/inboundEmailInApp.webhooks.integration.test.ts --reporter=dot- Blocker in local env: Vitest startup fails because
vitestpackage is not installed in this worktree.
- (2026-02-25) Targeted workflow destination routing integration test attempted:
cd server && npx vitest run src/test/integration/resolveInboundTicketContext.destinationRouting.integration.test.ts --reporter=dot- Blocker in local env: Vitest startup fails because
vitestpackage is not installed in this worktree.
- (2026-02-25) Targeted resolver unit test attempted:
npx vitest run shared/workflow/actions/__tests__/emailWorkflowActions.destinationResolver.test.ts --reporter=dot- Blocker in local env: Vitest startup fails because
dotenvpackage is missing from active root node_modules resolution path.
- (2026-02-25) Targeted clients actions unit test attempted:
npx vitest run packages/clients/src/actions/inboundTicketDestinationActions.test.ts --reporter=dot- Blocker in local env: Vitest startup fails because
dotenvpackage is missing from active root node_modules resolution path.
- (2026-02-25) Targeted ClientDetails inbound destination wiring test attempted:
npx vitest run packages/clients/src/components/clients/ClientDetails.inboundDestination.wiring.test.ts --reporter=dot- Blocker in local env: Vitest startup fails because
dotenvpackage is missing from active root node_modules resolution path.
- (2026-02-25) Targeted ContactDetails inbound destination wiring test attempted:
npx vitest run packages/clients/src/components/contacts/ContactDetails.inboundDestination.wiring.test.ts --reporter=dot- Blocker in local env: Vitest startup fails because
dotenvpackage is missing from active root node_modules resolution path.
Links / References
- In-app inbound pipeline:
shared/services/email/processInboundEmailInApp.ts
- Workflow action registration and context resolution:
shared/workflow/runtime/actions/registerEmailWorkflowActions.ts
- Shared email workflow actions:
shared/workflow/actions/emailWorkflowActions.ts
- Existing client inbound domain actions:
packages/clients/src/actions/clientInboundEmailDomainActions.ts
- New inbound destination actions:
packages/clients/src/actions/inboundTicketDestinationActions.ts
- Client details UI:
packages/clients/src/components/clients/ClientDetails.tsx
- Contact UI surfaces:
packages/clients/src/components/contacts/*
Open Questions
- Should client/contact destination be global, or optionally provider-specific for tenants with multiple inbound mailboxes?
- Should UI block selecting inactive defaults profiles, or allow save and rely on runtime fallback?
- Should contact override be shown only when contact has a valid email value?
Implementation Log
- (2026-02-25) Marked
F001implemented infeatures.jsonand committed as the first checklist checkpoint. - (2026-02-25) Completed
F002by adding migration20260225120000_add_client_inbound_ticket_defaults_id.cjsto persistclients.inbound_ticket_defaults_id. - (2026-02-25) Completed
F003by adding migration20260225120500_add_contact_inbound_ticket_defaults_id.cjsto persistcontacts.inbound_ticket_defaults_id. - (2026-02-25) Completed
F004by adding migration20260225121000_add_inbound_ticket_defaults_lookup_indexes.cjswith safe create/drop index behavior. - (2026-02-25) Completed
F005by adding shared resolver logic inshared/workflow/actions/emailWorkflowActions.tsand wiring it into:shared/services/email/processInboundEmailInApp.tsshared/workflow/runtime/actions/registerEmailWorkflowActions.ts
- (2026-02-25) Completed
F006by passing domain-matched client context into the shared resolver in both in-app and workflow-runtime paths. - (2026-02-25) Completed
F007by enforcing tenant+active validation on contact/client destination IDs before applying defaults. - (2026-02-25) Completed
F008by using shared destination resolution inprocessInboundEmailInAppbefore new-ticket create, while preserving existing reply-token/thread branches. - (2026-02-25) Completed
F009by using the same shared destination resolver inresolve_inbound_ticket_context(runtime v2 action registry) for parity with in-app processing. - (2026-02-25) Completed
F010by keeping reply-token/thread-header branches untouched; only new-ticket destination selection path was changed. - (2026-02-25) Completed
F011by retaining existing sender identity matching semantics:- exact contact remains the only source for comment authorship
- domain matching remains explicit via
client_inbound_email_domains
- (2026-02-25) Completed
F012by adding server actions for inbound destination option reads and client destination updates with explicit client permission checks. - (2026-02-25) Completed
F013by adding contact destination update action with explicit contact permission checks and tenant-scoped destination validation. - (2026-02-25) Completed
F014by adding an inbound ticket destination selector toClientDetailswith clear-to-provider-default behavior. - (2026-02-25) Completed
F015by adding optional contact override selectors to:packages/clients/src/components/contacts/ContactDetails.tsxpackages/clients/src/components/contacts/ContactDetailsEdit.tsx
- (2026-02-25) Completed
F016by adding explicit precedence helper text in both client and contact destination controls:- Contact override -> Client destination -> Provider default
- (2026-02-25) Completed
F017by emitting structured resolution logs:- destination source (
contact_override,client_default_from_contact,client_default_from_domain,provider_default) - fallback warnings with configured invalid destination IDs and fallback reason
- destination source (
- (2026-02-25) Completed
F018by adding in-app integration cases inserver/src/test/integration/inboundEmailInApp.webhooks.integration.test.tsfor:- exact contact override destination routing
- exact contact using client destination
- domain-matched client destination
- unmatched sender using provider destination
- (2026-02-25) Completed
F019by adding workflow-runtime integration coverage in:server/src/test/integration/resolveInboundTicketContext.destinationRouting.integration.test.ts- includes direct parity assertion against
processInboundEmailInAppfor identical sender/provider input
- (2026-02-25) Completed
F020by extending regression coverage:- explicit board-unchanged assertions on reply-token and thread-header reply flows
- existing idempotency tests for duplicate reply/new-email processing remain in place and continue to validate dedupe behavior
- (2026-02-25) Completed
T001by adding migration integration coverage inserver/src/test/integration/inboundTicketDestinationMigrations.integration.test.tsto assertclients.inbound_ticket_defaults_idexists, is nullable, and is UUID-typed. - (2026-02-25) Completed
T002by extending the same migration integration suite to assertcontacts.inbound_ticket_defaults_idexists, is nullable, and is UUID-typed. - (2026-02-25) Completed
T003by adding index coverage in the same migration integration suite, asserting:idx_clients_tenant_inbound_ticket_defaultsonclients(tenant, inbound_ticket_defaults_id)idx_contacts_tenant_inbound_ticket_defaultsoncontacts(tenant, inbound_ticket_defaults_id)
- (2026-02-25) Completed
T004by adding rollback-path coverage in the same migration integration suite:- executes migration
downfunctions inside a transaction and asserts both columns/indexes are removed - executes migration
upfunctions inside the same transaction and asserts both columns/indexes are restored - rolls back transaction to avoid mutating shared integration DB state
- executes migration
- (2026-02-25) Completed
T005by adding shared resolver unit coverage inshared/workflow/actions/__tests__/emailWorkflowActions.destinationResolver.test.tsfor precedence path:- exact sender contact + contact override resolves
source=contact_override - returns contact override defaults and does not consult client defaults
- exact sender contact + contact override resolves
- (2026-02-25) Completed
T006by extending shared resolver unit coverage for precedence path:- exact sender contact with no override resolves
source=client_default_from_contact - uses the matched contact's client
inbound_ticket_defaults_id
- exact sender contact with no override resolves
- (2026-02-25) Completed
T007by extending shared resolver unit coverage for domain path:- when no exact contact is provided and domain client is matched, resolves
source=client_default_from_domain - returns domain client's active defaults
- when no exact contact is provided and domain client is matched, resolves
- (2026-02-25) Completed
T008by extending shared resolver unit coverage for fallback path:- when neither exact-contact nor domain destination applies, resolves
source=provider_default - returns provider defaults unchanged with no fallback reason
- when neither exact-contact nor domain destination applies, resolves
- (2026-02-25) Completed
T009by extending shared resolver unit coverage for invalid override safety:- invalid/inactive contact override destination falls back to
source=provider_default - emits
fallbackReason=invalid_or_inactive_contact_override
- invalid/inactive contact override destination falls back to
- (2026-02-25) Completed
T010by extending shared resolver unit coverage for invalid client destination safety:- invalid/inactive client destination (from exact-contact client path) falls back to
source=provider_default - emits
fallbackReason=invalid_or_inactive_client_default_from_contact
- invalid/inactive client destination (from exact-contact client path) falls back to
- (2026-02-25) Completed
T011by confirming in-app integration coverage exists inserver/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts:- test case
Routing destination: exact sender contact override uses contact override defaults boardasserts board/client/contact routing from contact override defaults.
- test case
- (2026-02-25) Completed
T012by confirming in-app integration coverage exists inserver/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts:- test case
Routing destination: exact sender without contact override uses contact's client destination defaultsasserts exact-contact fallback to client destination defaults.
- test case
- (2026-02-25) Completed
T013by confirming in-app integration coverage exists inserver/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts:- test case
Routing destination: unknown sender + domain-matched client uses domain client destination defaultsasserts domain-matched unknown-contact routing.
- test case
- (2026-02-25) Completed
T014by confirming in-app integration coverage exists inserver/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts:- test case
Unmatched sender: system follows the defined behavior without throwingasserts provider-default board/client fallback for unmatched routing.
- test case
- (2026-02-25) Completed
T015by confirming workflow integration coverage exists inserver/src/test/integration/resolveInboundTicketContext.destinationRouting.integration.test.ts:- test case
returns contact override destination outcome for exact senderassertsresolve_inbound_ticket_contextreturns contact-override destination.
- test case
- (2026-02-25) Completed
T016by confirming workflow integration coverage exists inserver/src/test/integration/resolveInboundTicketContext.destinationRouting.integration.test.ts:- test case
returns client's destination outcome when exact sender has no contact overrideasserts workflow action returns client-default destination.
- test case
- (2026-02-25) Completed
T017by confirming workflow integration coverage exists inserver/src/test/integration/resolveInboundTicketContext.destinationRouting.integration.test.ts:- test case
returns domain-matched client destination outcome when sender is unknown contactasserts domain-matched destination resolution.
- test case
- (2026-02-25) Completed
T018by confirming workflow/in-app parity coverage exists inserver/src/test/integration/resolveInboundTicketContext.destinationRouting.integration.test.ts:- test case
matches in-app destination selection for the same sender/provider inputcompares runtime action output withprocessInboundEmailInAppticket results.
- test case
- (2026-02-25) Completed
T019by confirming reply-token regression coverage exists inserver/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts:- test case
Reply threading: reply token resolves ticket and creates exactly 1 new commentnow asserts ticket board remains unchanged after reply threading.
- test case
- (2026-02-25) Completed
T020by confirming thread-header regression coverage exists inserver/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts:- test case
Reply threading: thread headers resolve ticket and create exactly 1 new commentnow asserts ticket board remains unchanged after thread-header matching.
- test case
- (2026-02-25) Completed
T021by confirming exact-contact matching regression coverage exists inserver/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts:- test case
Contact match: sender email is normalized from display-name formatasserts contact/client IDs still resolve from exact sender match semantics.
- test case
- (2026-02-25) Completed
T022by confirming explicit-domain matching regression coverage exists inserver/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts:- test case
Domain fallback: does not match by domain unless the domain is explicitly configuredasserts no inferred domain ownership from contact records.
- test case
- (2026-02-25) Completed
T023by addingpackages/clients/src/actions/inboundTicketDestinationActions.test.tscoverage for client destination actions:- rejects updates without
client:updatepermission - rejects cross-tenant destination IDs via tenant-scoped
inbound_ticket_defaultsvalidation
- rejects updates without
- (2026-02-25) Completed
T024by extendingpackages/clients/src/actions/inboundTicketDestinationActions.test.tscoverage for contact destination actions:- rejects updates without
contact:updatepermission - rejects cross-tenant destination IDs via tenant-scoped
inbound_ticket_defaultsvalidation
- rejects updates without
- (2026-02-25) Completed
T025by addingpackages/clients/src/components/clients/ClientDetails.inboundDestination.wiring.test.ts:- verifies client UI destination select wiring (
value,allowClear,onValueChange) - verifies persistence path calls
updateClient(...)and that server-side client update normalizes cleared empty strings tonull
- verifies client UI destination select wiring (
- (2026-02-25) Completed
T026by:- extending
packages/clients/src/actions/contact-actions/contactActions.tsxto persistinbound_ticket_defaults_idupdates, normalize clears tonull, and tenant-validate destination IDs - adding
packages/clients/src/components/contacts/ContactDetails.inboundDestination.wiring.test.tsto verify contact UI set/clear wiring and save-path persistence throughupdateContact(...)
- extending
- (2026-02-25) Completed
T027by extendingshared/workflow/actions/__tests__/emailWorkflowActions.destinationResolver.test.ts:- asserts
console.warnmetadata includessource=contact_overrideandfallback=provider_defaultfor invalid override fallbacks - asserts terminal
console.debugmetadata includessource=provider_defaultwithfallbackReason=invalid_or_inactive_contact_override
- asserts
- (2026-02-25) Completed
T028by extendingserver/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts:- added routed idempotency case
Idempotency: replay same routed contact-override email does not create duplicate routed tickets - asserts first run creates routed ticket on override board and second run dedupes, preserving single ticket/comment under sender-based routing
- added routed idempotency case