# HANDOFF ## ═══════ SESSION 2026-08-22 — Final decision: external n8n (Option 3) ═══════ - User selected Option 3: n8n remains a separate service for the LLM chatbot/workflow path. - Do not integrate Hermes, embed n8n, or change Chatwoot production code for this decision. - Keep the existing AgentBot async webhook boundary: Chatwoot → external n8n → configured LLM/external services → Chatwoot API reply. - The Gateway/Adapter design is deferred; implement it only if later requirements justify centralized tenant isolation, idempotency, DLQ, observability, or provider routing. - Only this handoff/log decision is updated; production source remains unchanged. ## ═══════ SESSION 2026-08-22 — LLM chatbot runtime architecture discovery ═══════ - Read-only architecture review for `/Users/kunthawat/Gitea/Chatwoot`; no production code changed. - Chatwoot's existing AgentBot path is already the intended boundary: async signed webhook out, external runtime processes, then external runtime posts an outgoing message through Chatwoot API. `outgoing_url` is not an LLM request/response endpoint. - Hermes is technically feasible only behind a bridge. Hermes API server is the preferred execution interface; Hermes webhook adapter is not plug-compatible because signature headers and response delivery contract differ. - Recommended target: `Chatwoot AgentBot → dedicated Hermes bridge → Hermes API → Chatwoot API`; optional external n8n remains for visual workflows, CRM/LINE/ERP side effects, and integrations. Do not embed n8n or give a customer-facing Hermes profile unrestricted terminal/file tools. - Tenant rules: keep Chatwoot as transcript/source of truth; namespace every request by account/inbox/conversation; do not treat Hermes personal memory/profile files as a multi-tenant knowledge base. Use allow-listed tools, sandboxing, idempotency, HMAC verification, secret rotation, and human handoff. - Next implementation gate: P0 one-account bridge proof (or n8n→Hermes proof), message-created only, no loop, signed webhook verification, reply via bot token, duplicate/retry tests, cross-tenant isolation test, and latency/error telemetry. - Working tree was already dirty with unrelated M1/M2 changes; future bridge work must use a separate worktree/branch. ## ═══════ SESSION 2026-08-19 — Moreminimore Chat: rebrand + M1 + M2 work ═══════ ## Current state (2026-08-19) Repo `/Users/kunthawat/Gitea/Chatwoot`, branch `develop`, **pushed to Gitea**. Remote origin = `ssh://git@moreminimoreapps-gitea.ahkhwd.easypanel.host:2222/kunthawat/moreminimore-chat` (NOT github). SSH push via port **2222** (the HTTPS path git.moreminimore.com gets nginx 413; SSH port 22 is not Gitea SSH). HEAD = `ccff2dfca`. Worktree clean except untracked `.hermes/plans/` (never commit). ### Completed this session (all through independent five-key reviews, deployed by user) - **Rebrand → Moreminimore Chat** complete (phases 1-3 + Dockerfile + brand DB fix): f17ce89, d925ab1, 18e6fc28a, bee28c32e, d77d60443, 4b35130ae. - **Docker/build**: root `Dockerfile` added (4b35130ae) so EasyPanel 'Dockerfile' builder works instead of Nixpacks rbenv stack. nixpacks.toml added then removed (wrong approach). - **M1 — per-account OpenAI custom base URL** (the /app/accounts/{id}/settings/integrations/openai page): commit f9628db0e. Adds optional custom OpenAI-compatible base_url per account (apps.yml schema+form, llm_base_service api_base priority hook.base_url→CAPTAIN_OPEN_AI_ENDPOINT→api.openai.com, key_validator base_url kwarg, hook https validation). NOTE: an earlier wrong-scope Captain-level provider switch (46fe70398) was reverted (961ecd388) — the Captain CAPTAIN_OPEN_AI_ENDPOINT already supports custom URL as original behavior, so no change needed there. - **M2 — analytics foundation** (self-improving chatbot, admin-only): - 9fbb0da9b `conversation_daily_metrics` (per account/day; tags, deal outcomes, peak hours, agent/team/channel/inbox breakdown) - 0fad9ce6b `customer_daily_metrics` (per customer/day; agent_ids) - 161e4210b `product_catalog_entries` (per-account hierarchical product reference: group>subgroup>product+aliases) - ccff2dfca `lib/llm/resolver.rb` (LLM cascade: per-account openai hook → Captain → nil; https-only api_base) - **DB brand fix** done by user on prod (login no longer shows Chatwoot / logo loads). ### M2 design decisions (approved by user — IMPORTANT for remaining work) - Analyze EVERY chat of EVERY account daily (high LLM cost accepted — selling point is self-improving chatbot). - LLM cascade: per-account openai → fallback Captain config → if neither, feature disabled (LLM integration effectively always on unless Captain unset). - Use Chatwoot's ORIGINAL conversation tags (ActsAsTaggableOn tag_list) — do NOT add many tag fields. Product/sale tag goes into the same tag list, prefixed (e.g. lines like `group>subgroup>product` joined, or `สินค้า:...`/`กลุ่ม:...`). Daily metric `top_tags` aggregates from these. - **No `tagged_at`** (user decided to skip time-tracking to avoid DB complexity). - Per-day stats are IMMUTABLE snapshots; conversation live tags get re-tagged on later analysis (e.g. session now about product B → product A tag removed). The daily snapshot preserves the earlier state. - Report page **admin-role ONLY** (agent must NOT see deep analytics). Filter by agent/team/inbox/channel/tag/sale-tag/time + cross-analysis. Break down per CUSTOMER and per AGENT. - Weekly (Mon 10:00) LLM summarizes 7 days → persona improvement recommendation (summary only, NOT full prompt). Delivered via preferred channel: Line (has quick-replies) / Telegram (inline keyboard) / webhook (3rd party). Admin chooses approve / view-full-prompt / reject; viewing full prompt → re-approve; on approve → webhook with prompt (admin sets URL+secret). - Product list: per-account, admin-only menu (like report), import via copy/paste or CSV/XLSX. Multi-level hierarchy (big group>subgroup>product>display) — LLM matches from lowest level first, tags ancestors; supports multiple products/groups per chat. ### NEXT TO DO (M2 remaining — biggest remaining work) 1. **part2b-ii — LLM classifier service: ✅ DONE (2026-08-19)**. New `lib/llm/analytics_classifier.rb` — reads conversation, classifies via `Llm::Resolver` cascade + `with_schema` JSON output → `Result` (topics, products via tag_hierarchy path mapping, deal won/lost/undecided). Fail-closed on no-credential (returns `disabled`, never calls LLM / sends content). Also fixed: `Account` missing `has_many :product_catalog_entries` (foundation `161e4210b` wired model+migration but not the reverse association → would NoMethodError). Verified: `ruby -c` clean (2.6/3.4), isolated smoke 8/8 under Ruby 3.4.10 (found `/opt/homebrew/opt/ruby@3.4/bin/ruby` — real 3.4.10 available, `.ruby-version`=3.4.4), static scan clean, independent reviewer `deleg_47c1a2cb` = complete 5-key PASS (non-blocking em-dash escape fix applied). Plan doc: `.hermes/plans/2026-08-19-m2-self-improving-analytics.md`. 2. **part2b-iii — daily batch job: ✅ DONE (2026-08-19)**. New `app/services/analytics/account_daily_processor.rb` (per-account/date: UTC day window from `account.reporting_timezone`, selects active conversations, classifies via `Llm::AnalyticsClassifier` fail-closed, re-tags replacing M2-managed tags + preserving manual labels, upserts ConversationDailyMetric unique account+date + CustomerDailyMetric unique account+contact+date) + `app/jobs/analytics/daily_metrics_job.rb` (per-account rescue, yesterday in account TZ) + `config/schedule.yml` entry `analytics_daily_metrics_job` `30 2 * * *` timezone `Asia/Bangkok`. Verified: `ruby -c` clean (3.4.10), smoke 15/15, static scan clean, schedule no dup keys, reviewer `deleg_fecf7c61` = complete 5-key PASS (4 non-blocking suggestions: add specs, N+1 in chat_message_count, deal_outcomes dual structure, apply_tags rescue scope). 3. **phase 2 — rollup + report: ✅ DONE (2026-08-19)**. Backend: `Analytics::ReportService` (summary/timeseries/customers/agents from immutable daily-metric tables; deal_outcomes normalization for 'totals'-nested + flat) + `Analytics::DrilldownService` (deep filterable drilldown over live conversations: since/until/agent/team/inbox/channel/tag/deal + pagination; fail-closed label-join) + `Api::V2::Accounts::AnalyticsReportsController` (admin-only via ReportPolicy#view?=administrator; summary + drilldown) + routes `analytics_reports` (get :summary/:drilldown). `AccountDailyProcessor#apply_tags` now writes `deal:` tag for deal filtering. Frontend: `api/analyticsReports.js` + `AnalyticsReports.vue` (summary cards + filter bar + drilldown table) + route `analytics_reports` under reports section + `ANALYTICS_REPORTS` i18n keys in en/report.json. Verified: backend smoke 12/12, reviewers `deleg_6800b012` (backend) + `deleg_15f8447c` (frontend) = complete 5-key PASS (non-blocking i18n suggestions applied). NOTE: frontend full build (eslint/vite/vitest) cannot run locally (no node_modules) — run `pnpm install && pnpm test` before ship. 4. **phase 3 — import + persona eval + approval (2026-08-20):** - **Weekly persona eval** ✅: `Analytics::WeeklyPersonaEvaluator` (7d via ReportService → LLM summary+recommendations; summary-only, no PII) + `persona_evaluation` endpoint + route. Reviewer `deleg_b7d63fd7` PASS. - **Frontend product-catalog import UI** ✅: `api/productCatalog.js` + `productCatalog/Index.vue` (paste + xlsx/csv upload + result + table) + admin-only route. Reviewer `deleg_0e9f9291` PASS (i18n suggestions applied). - **Approval flow** (user chose **LINE primary + webhook fallback**, then added **Telegram**; precedence LINE → Telegram → webhook): `Analytics::PersonaApprovalService` (`deliver` LINE quick-reply / Telegram inline keyboard / webhook fallback; `notify_approval` webhook-only w/ decision) + `Analytics::WeeklyPersonaEvaluationJob` + controller (`persona_evaluation_deliver`, `persona_approval_settings` [line_user_id + telegram_chat_id + webhook_url, conditional], `persona_decision`) + routes + schedule.yml `analytics_weekly_persona_evaluation_job` (Mon 10:00 Asia/Bangkok). First reviewer `deleg_1fe19871` FAIL-CLOSED (1 medium logic error: `:approve` path LINE-first + decision not passed) → **FIXED** (webhook-only `notify_approval(decision)`) → re-review `deleg_3052f7a4` PASS; Telegram `deleg_34116d1a` PASS. Config: `persona_line_user_id` / `persona_telegram_chat_id` / `persona_webhook_url` / `persona_webhook_secret` in Account#custom_attributes. - **Docs** — via HANDOFF.md/plan doc; no separate docs/ convention. - **POST-MVP design note (prompt webhook handshake) — user clarified, NOT built, awaiting LLM-server details**: a SEPARATE LLM server holds/applies the actual prompt; this app connects via webhook — (1) connect-first: pull current prompt (GET) + store snapshot; (2) approve weekly recommendation → POST new prompt to LLM server to apply. Analytics here ONLY surfaces "what customers asked" + recommendations; knowledge-prep/prompt-authoring is admin/LLM-server side. Delivery-webhook design (REST 2-endpoint vs generic action-body) DEFERRED — user wants document/design only for now. Verified: smoke tests (eval 8/8, import 10/10 + xlsx 5/5, approval 10/10 incl. Telegram), `ruby -c` clean, node --check on .js, schedule.yml valid + unique keys, static scan clean across all. ### Verification notes / blockers - Ruby env active is 2.6.10; target 3.4.4. Rails/RSpec/RuboCop cannot be reliably claimed passed. Ruby `ruby -c` syntax + YAML validity + isolated smoke tests used instead. - Full Vitest baseline is clean under TZ=UTC: 414 files / 4176 tests, 0 failed. - Enterprise (EE) files still read CAPTAIN_OPEN_AI_ENDPOINT directly and won't honor per-account base_url gating — intentional (out of OSS scope), note as follow-up. - No secrets committed. Temporary test helpers under /tmp only. ## ───────── historical (2026-08-16, before this session) ───────── ### Latest handoff — 2026-08-16 (updated after rebrand + push) - Rebrand to **Moreminimore Chat** complete across all phases (committed): `f17ce89` (phase 1 configurable brand core: logo/config/mailer defaults/views), `d925ab1` (phase 2a cleanup + 512px thumbnail), `18e6fc28a` (phase 2b color theme: black #333333 primary + Moreminimore yellow #FED400 accent), `bee28c32e` (phase 3 visible surfaces: app.json + mailers + 56 locales + prompts). All phases passed independent five-key reviews (deleg_a733cdf4, deleg_37455fbd, deleg_9b054593, deleg_892d115f). - **PUSHED to Gitea** via SSH port 2222: branch `develop` (HEAD `bee28c32e`) now on `kunthawat/moreminimore-chat`. Remote `origin` = `ssh://git@moreminimoreapps-gitea.ahkhwd.easypanel.host:2222/kunthawat/moreminimore-chat`. HTTPS push to `git.moreminimore.com` is blocked by nginx 413 (client_max_body_size < ~237MB pack); SSH port 22 is NOT Gitea SSH — use 2222. `upstream` (github.com/chatwoot) removed; Gitea rejects shallow push, so unshallow first. SSH key `moreminimore-local-ed25519` added via Gitea API. - Earlier checkpoints: `19dc449` SM-07, `2ef6fa5` SM-07 remediation (approved deleg_087e4a3f), `364e72f` SM-08, `8101395` residual Hub removal (approved deleg_49d6ee2e + deleg_58a576e1). ### Verified checkpoints - `8ebb320` — privacy/branding audit harness. - `832a7fd` — Hub sync/registration/telemetry removal. - `1a3697f` — restored only the compatibility APIs required by direct push/billing callers. - `8b1a033` — removed the Chatwoot Hub push relay; exact post-commit independent review passed. - `3458272` — removed cwctl telemetry from the installer; exact post-commit independent review passed. - `d9bf4c4` — removed community signup/onboarding website-branding enrichment; split exact post-commit reviews `deleg_5e2578c5` (backend) and `deleg_ee3e217f` (frontend) passed with empty blocking arrays. - `ced77af` — SM-06 Product Analytics removal and dashboard config allowlist remediation; pre-commit review `deleg_f9644984` and exact post-commit review `deleg_9b0a2f63` passed with empty security/logic arrays. ### Current work / uncheckpointed work - SM-05 community signup enrichment removal is checkpointed at `d9bf4c4`; pre-commit review, commit hooks, and split exact post-commit reviews passed. The enterprise service/spec remain unchanged and fail with `NameError: uninitialized constant WebsiteBrandingService`. This remains an explicit architecture/legal blocker; do not add a dummy service or restore remote enrichment. - SM-06 is verified at `ced77af`. Its follow-up fix applies `.slice(*GLOBAL_CONFIG_KEYS)` before merging `app_config`, preventing stale/unrequested values such as `CLOUD_ANALYTICS_TOKEN` from reaching serialized `window.globalConfig`. SM-07 local-only observability and the remaining SM-08 dashboard/config script-injection work are still uncheckpointed. The community Help Center SM-08 correction is committed as `5619cc3`: remove GTM remote-container execution, reject/remove legacy GTM config, and retain only fixed allow-listed provider snippets for explicit admin configuration. Exact post-commit review `deleg_dd3db5b7` passed with empty blocking arrays. Enterprise marketing conversion tracking remains outside this community scope pending legal/compatibility review. ### Evidence and blockers - SM-03 exact post-commit review passed for `1a3697f..8b1a033`; reviewer suggestions are to add explicit missing/blank/partial Firebase negative coverage and clarify legacy FCM configuration names in `.env.example`. - SM-04 exact post-commit review passed for `1a3697f..3458272`; Bash 3.2 compatibility and fail-closed privacy-test behavior were verified. - SM-05 exact committed range `3458272..d9bf4c4` passed split independent reviews: backend `deleg_5e2578c5` and frontend `deleg_ee3e217f`; both returned complete five-key verdicts with empty security and logic arrays. Frontend targeted run passed 9 suites/8 tests. Backend RSpec rerun is environment-blocked because Ruby 3.4.4 is required but only 3.4.10 is installed and the bundle has no usable `rspec` executable. - SM-06 exact committed range `a96b977..ced77af` changed one controller line and passed fresh pre-commit/post-commit independent review. Raw merge leaked a stubbed analytics token; the allowlist slice now removes unrequested keys before layout serialization. Committed Ruby syntax, diff check, and security scan passed. Rails RSpec/RuboCop remain blocked because the active Ruby is 2.6.10 and Bundler 2.5.16 is unavailable. - SM-08 exact post-commit review `deleg_dd3db5b7` passed for `d9bf4c4..5619cc3`; 57 portal model/controller examples, GTM regression 3/3, Ruby/ERB syntax, RuboCop, ESLint/Prettier, Vite build and privacy-audit harness passed. Full Vitest with `TZ=UTC` passes 414 files / 4176 tests, 0 failed. - No push or deploy has occurred. - Approved product name, domains, logos/icons and sender/legal values are still missing; stop before SM-09/10. - Full Rails/frontend suites, production-like DB checks, HTTP/DNS egress capture, `git fetch --unshallow`, and upstream merge rehearsal remain outstanding. The full Vitest frontend baseline is clean under `TZ=UTC`: 414 files / 4176 tests passed, 0 failed (the earlier "407/15 failed" record was a non-UTC run artifact; timezone-sensitive tests all pass under UTC). - Temporary test services/configuration stay under `/tmp`; never commit them or secrets. ### Repository baseline - Path: `/Users/kunthawat/Gitea/Chatwoot` - Origin: `https://github.com/chatwoot/chatwoot.git` - Branch: `develop` - Initial baseline commit: `9a73c1473ffa0ae6a9c7725046b8ca17922dcc83` - Latest checkpoint: `ced77af` (SM-06 Product Analytics plus dashboard config allowlist remediation; exact post-commit review `deleg_9b0a2f63` passed). SM-08 Help Center remains separately verified at `5619cc3`. - Clone is shallow; fetch full upstream history before merge/rebase rehearsal. ### Primary deliverable - `.hermes/plans/2026-08-15_092534-chatwoot-private-rebrand.md` - Small-model execution index: `.hermes/plans/chatwoot-private/README.md` - The execution package contains `00-execution-contract.md` plus runbooks `01`–`12` with 58 unique `SM-*` tasks. Send only one task at a time and review its exact diff/test evidence before advancing. ### Most important finding `DISABLE_TELEMETRY=true` is not a complete opt-out. `lib/chatwoot_hub.rb` still posts `instance_config` to the Hub; it only omits `instance_metrics`. The corresponding behavior is asserted in `spec/lib/chatwoot_hub_spec.rb`. ### Other unsolicited/vendor-controlled egress to remove - Chatwoot Hub daily sync and onboarding registration - Chatwoot Hub `/send_push` relay fallback - Hub-hosted changelog fetch - silent cwctl command reporting to the Hub - automatic signup email-domain website/DNS branding enrichment - Amplitude browser analytics - Sentry/frontend/backend remote error reporting - optional remote APM agents - arbitrary dashboard scripts and remote GTM container execution - enterprise marketing conversion tracking (proprietary/cloud-only; pending separate legal/compatibility decision) ### Implementation order 1. Follow dependency order in `.hermes/plans/chatwoot-private/README.md`. 2. Establish the baseline and privacy audit harness before behavior changes. 3. Remove Hub sync/registration/manual refresh, push relay, cwctl telemetry, signup-domain enrichment and remote changelog. 4. Neutralize/remove analytics, remote error reporting/APM and script injection. 5. Stop at the Runbook 09 input gate until approved brand values/assets are supplied. 6. Add central brand defaults and replace visible assets/copy without renaming compatibility APIs. 7. Run static and isolated dynamic egress gates, positive feature-traffic tests, full review and visual QA. 8. Complete upstream-history and merge-rehearsal workflow; never mix upstream merge with customization commits. ### User decisions required before implementation - Product name, canonical/support/docs domains, logos/icons and email sender name - Private Gitea repository name - Whether to keep deprecated widget API aliases (recommended: yes) - Whether local-only logs are sufficient or a self-hosted observability endpoint is required - Push notification plan and own FCM/VAPID credentials ### Safety/legal notes - Root is MIT, but `enterprise/` is proprietary; do not ship enterprise features without a separate license. - No remote repository was created, no files were pushed and no deployment was performed.