logicspike/docs

Contact Intelligence

Contact Intelligence — Backlog & Scorecard

Last updated: 2026-06-29 Analyst: Codebase audit (full source + spec review)


Phase Status

Phase What Status
Phase 1 Schema — 5 new tables + 11 new columns ✅ COMPLETE — migrations 0001–0005 applied, all tables live
Phase 2 Wrangler bindings — CONSOLIDATION_QUEUE + weekly cron ✅ COMPLETE — queue active, Sunday 2am UTC cron
Phase 3 Lib files — extraction, entity-graph, consolidation, relationship ✅ COMPLETE
Phase 4 Routes — L1/L2 context, session-close ingest, /correct, queue consumer ✅ COMPLETE
Phase 5 CE integration — update orchestrator to call GET /context/:id instead of /mood ❌ NOT STARTED — CE still calls /mood stub
Phase 6 Dashboard — contact list, memory viewer, correction UI ❌ NOT STARTED

Scorecard

# Parameter Score Notes
1 Multi-tenancy 10.0 Every query scoped by tenant_id; no cross-tenant data leakage possible
2 Data Model 9.5 12 well-designed tables, correct indexes, CASCADE deletes, pgvector for embeddings, 1:1 state table
3 GDPR Compliance 9.5 Soft-delete + hard-delete of memories; data export endpoint; outreach_disabled flag honoured globally
4 Performance 9.0 Context hot path uses 5 parallel queries (< 30ms target); mood-only path < 5ms; async interaction logs
5 Security 8.5 PBAC with contacts: namespace; gateway key header auth; no SQL injection surface
6 API Design 8.5 Clean RESTful routes; consistent response shapes; proper HTTP status codes; paginated lists
7 Business Logic 8.5 Mood classifier with keyword fallback; 6-stage relationship state machine; churn risk formula; outreach scanner with pre-delivery checks
8 Integration 8.0 Clean context/ingest contract with Chat Engine; LLM multi-provider (Anthropic → Gemini → OpenAI)
9 Testing 8.0 86 vitest tests across 6 files (contacts, context, ingest, outreach, mood, relationship); PGlite integration DB; gap: extraction/entity-graph/consolidation lib files have no tests
10 Background Jobs 7.5 Cron every 6h (outreach scan); weekly consolidation queue active
11 Error Handling 7.5 LLM timeout falls back to keyword classifier; outreach pre-delivery checks for all terminal states; fire-and-forget logging
12 Documentation 8.0 Eight docs (README, api-spec, memory-spec, domain-model, outreach-spec, architecture, user-journey, backlog); current as of 2026-06-29
13 Feature Completeness 7.0 L1/L2 context complete; session-close extraction complete; memory correction complete; Phase 5 CE integration not started
14 Observability 5.5 ci_interaction_logs table exists; no structured logging, Worker tail, or alerting

Overall: 8.2 / 10


Bugs

B1 — L2 semantic search returns null for first call after new ingest

Severity: Low (expected async behavior, documented) Status: PARTIALLY RESOLVED — embeddings now generated; timing gap remains

Previously: Embeddings were never generated at all (queue binding commented out). The feature was silently broken.

Now: Embeddings are generated via c.executionCtx.waitUntil() in the ingest route (fire-and-forget). The first GET /context/:id?message=... call after a new ingest returns message_relevance: null because the embedding hasn't landed yet. A second call a few seconds later will return the semantic result.

Remaining gap: No retry queue for failed embedding generation. If the Gemini/OpenAI call fails, that memory's embedding stays null permanently (no dead-letter path).


B2 — Memory importance never decays

Severity: Medium Status: OPEN

The decay function (importance *= (1 - decayRate)^days) is implemented in the consolidation pipeline (step 1 of 9). The weekly cron enqueues work to CONSOLIDATION_QUEUE, and the queue consumer runs the pipeline. However, the consolidation job has not been stress-tested in production — decay has not been observed for existing contacts. Old memories may retain full importance until the first successful consolidation run.


B3 — Outreach message is generated but never delivered

Severity: High (feature silently broken) Status: OPEN (Phase 5 dependency)

The outreach scanner generates messages and marks triggers fired, but delivery requires calling CHAT_ENGINE_SERVICE.fetch() to push the message to the bot's channel. CHAT_ENGINE_SERVICE binding exists in wrangler.toml but the delivery call is not implemented in the scanner — it stores the message in outreach_triggers.message and marks the trigger fired without actually sending anything. Phase 5 must implement this.


B4 — Milestone detector has no idempotency lock

Severity: Low Status: OPEN

The milestone check is an exact match (WHERE total_messages = 100). If two cron cycles fire within seconds of each other, both runs find total_messages = 100 and create duplicate milestone triggers before either inserts. The contact receives two identical messages.

Fix: insert the trigger with a unique constraint on (contact_id, trigger_type, milestone_value) and use ON CONFLICT DO NOTHING.


B5 — Entity names are case-sensitive

Severity: Low Status: OPEN

The ci_entities unique index is on (contact_id, type, name) with no normalization. "Bruno" and "bruno" create separate entity rows. The upsertEntity() function does fuzzy dedup (Levenshtein ≥ 0.85) in the weekly consolidation pass, but the case-split persists until that job runs.


B6 — Recurring trigger type has no scheduler

Severity: Medium Status: OPEN

The schema defines trigger_type: "recurring" and outreach_config has max_per_day/min_gap_days, but the outreach scanner has no createRecurringTriggers() step. The recurring_cron expression field in the old spec is not in the actual config table. This feature is non-functional.


B7 — Fire-and-forget interaction log silently drops write failures

Severity: Low Status: OPEN

Interaction log insertion is kicked off via waitUntil() without a catch that bubbles metrics. A DB error (connection timeout, schema mismatch) silently disappears. Analytics counts become under-counted with no alert.


Areas of Improvement

A1 — Zero test coverage ✅ RESOLVED

Resolution (2026-06-29): 86 vitest tests added across 6 files.

File Tests What's covered
contacts.test.ts 22 CRUD, soft delete, correct, memories, tenant isolation
context.test.ts 11 L1 cold-start, response shape, /mood
ingest.test.ts 9 Per-message path, session_close, duplicate prevention
outreach.test.ts 12 Create, list, cancel, config CRUD
mood.test.ts 13 Pure function, all moods, case-insensitive, confidence range
relationship.test.ts 19 computeRelationshipStage × 9, computeChurnRisk × 7, updateStreak × 4

Run tests: cd apps/contact-intelligence && npm test

Remaining gap: src/lib/extraction.ts, src/lib/entity-graph.ts, and src/lib/consolidation.ts have no unit tests. These are the most complex lib files and the most likely to have regression risk.


A2 — No database-level RLS

All isolation is app-level (WHERE tenant_id = ?). A route that forgets the filter (or a new developer adding a direct query) leaks cross-tenant data. Adding RLS policies on all CI tables would guarantee isolation as a defense-in-depth layer.


A3 — Cross-channel contact merging not implemented

A customer on WhatsApp AND Telegram is two separate CI contacts. There is no link, no shared memory. When they switch channels, they start fresh. ci_contact_channels was added as the data model for this feature, but no merge logic exists.


A4 — Language detection and adaptation absent

CI has no language detection. The bot responds in one language regardless of the contact's messages. Adding detection from the first 3 messages and storing it as a preference would allow Chat Engine to respond in the contact's preferred language.


A5 — Preferred hours inference not automated

outreach_config.quiet_hours_start/end is tenant-wide and static. ci_interaction_logs.created_at records every message timestamp. A background job could compute per-contact preferred contact hours from this log — making outreach arrive when the customer is actually online.


A6 — Crisis detection not fully implemented

The crisis_level field exists on ci_contacts and is returned in context responses. The session-close LLM extraction is supposed to set it, but the extraction prompt doesn't explicitly instruct the model to evaluate crisis signals. The field stays at "none" for most contacts regardless of conversation content.


GET /contacts supports channel, stage, sort, limit, offset — but no search parameter. Finding a specific contact by name requires paginating through the full list.


Deferred (not bugs, notable gaps)

Item Status
Phase 5: CE orchestrator calls GET /context/:id Not started — CE still calls /mood
Phase 6: Contact intelligence dashboard Not started
Recurring outreach scheduler Not implemented (B6)
Delivery feedback loop (read receipts) Not implemented
Timezone inference per contact from activity Not implemented
Embedding retry queue Not implemented (B1 gap)
Contact Intelligence