> ## Documentation Index
> Fetch the complete documentation index at: https://mako-docs.devinagiffy.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Context memory architecture

# Context Memory System — Architecture

Passive intelligence layer that listens to Slack channel messages, extracts structured facts using Claude Haiku, stores them as vector-embedded observations, and synthesizes per-entity summaries nightly. The bot uses these at query time to inject relevant organizational context into LLM prompts.

***

## 1. Data Flow — End to End

```mermaid theme={null}
flowchart TD
    subgraph Sources
        SLACK[Slack Channels]
        URL[URLs in Messages]
        SEARCH[Web Search / Tavily]
        ADMIN[Admin Manual Input]
        BACKFILL[Historical Backfill]
    end

    subgraph Ingestion
        BOT[Bot Adapter<br/>fire-and-forget]
        API_INGEST[POST /context/ingest]
        BUFFER[(channel_messages<br/>temporary buffer)]
    end

    subgraph "Batch Processing (BullMQ)"
        TRIGGER{Batch trigger:<br/>20 msgs OR 30-min timer}
        HAIKU[Claude Haiku 4.5<br/>Fact Extraction]
        EMBED[OpenAI text-embedding-3-small<br/>1536-dim vectors]
        URL_FETCH[Jina Reader API<br/>URL content fetch]
        WEB_SEARCH[Tavily API<br/>Web search]
        ACTIONS[Context Actions Engine<br/>Proactive alerts]
    end

    subgraph Storage
        OBS[(context_observations<br/>facts + embeddings)]
        SUM[(context_summaries<br/>entity profiles)]
    end

    subgraph "Scheduled Crons (per-org)"
        DAILY_SUM[context-daily-summary<br/>11 PM]
        CLEANUP[context-cleanup<br/>2 AM]
    end

    subgraph "Query Time"
        USER_MSG[User sends message]
        CHAT[ChatService]
        SEM_SEARCH[pgvector similarity search]
        LLM_PROMPT[Inject context into LLM prompt]
    end

    SLACK -->|every message| BOT
    BOT -->|fire-and-forget POST| API_INGEST
    ADMIN -->|POST /context/observations| OBS
    API_INGEST --> BUFFER

    BUFFER --> TRIGGER
    TRIGGER -->|process-batch job| HAIKU
    HAIKU -->|JSON array of observations| EMBED
    EMBED -->|vector + metadata| OBS
    HAIKU --> ACTIONS

    BUFFER -->|URLs detected| URL_FETCH
    URL_FETCH -->|content| HAIKU
    SEARCH --> WEB_SEARCH
    WEB_SEARCH -->|results| OBS

    BACKFILL -->|Slack API history| BUFFER

    DAILY_SUM -->|synthesize profiles| SUM
    CLEANUP -->|prune older than 7 days| BUFFER

    OBS --> DAILY_SUM

    USER_MSG --> CHAT
    CHAT --> SEM_SEARCH
    SEM_SEARCH -->|top 5 observations| LLM_PROMPT
    SUM -->|user profile summary| LLM_PROMPT
    OBS -.->|cosine similarity via HNSW| SEM_SEARCH

    ACTIONS -->|deadline_mention| ALERT_OPS[Alert Ops Channel]
    ACTIONS -->|commitment| AUTO_TASK[Auto-Create Task]
    ACTIONS -->|risk_signal| BLOCKER[Create Blocker]
    ACTIONS -->|mood_signal| FLAG_OPS[Flag to Ops]
    ACTIONS -->|handoff| DM_USER[DM User]
```

***

## 2. Entity Relationship Diagram

```mermaid theme={null}
erDiagram
    channel_messages {
        uuid id PK
        uuid organization_id FK
        varchar channel_id
        varchar channel_name
        uuid user_id FK "nullable"
        varchar platform_user_id
        varchar message_ts
        varchar thread_ts "nullable"
        text raw_text
        jsonb urls "string[]"
        timestamp processed_at "nullable — null = unprocessed"
        timestamp created_at
    }

    context_observations {
        uuid id PK
        uuid organization_id FK
        varchar entity_type "user | client | channel | url | search"
        varchar entity_id "UUID or platform ID or name"
        varchar category "commitment | blocker | handoff | risk_signal | mood_signal | deadline_mention | reference | status_update"
        text observation
        vector_1536 embedding "OpenAI text-embedding-3-small"
        real confidence "0.5 - 1.0"
        varchar source_type "channel_message | url_fetch | web_search | backfill | admin_input"
        jsonb source_message_ids "string[]"
        text source_url "nullable"
        date source_date
        timestamp expires_at "nullable"
        uuid superseded_by "nullable — soft delete"
        timestamp created_at
    }

    context_summaries {
        uuid id PK
        uuid organization_id FK
        varchar entity_type "user | client | channel"
        varchar entity_id
        varchar summary_type "profile"
        text summary "structured markdown profile"
        vector_1536 embedding
        jsonb metadata "nullable"
        date period_start "nullable"
        date period_end "nullable"
        timestamp created_at
        timestamp updated_at
    }

    channel_messages ||--o{ context_observations : "source_message_ids"
    context_observations }o--|| context_summaries : "aggregated nightly into"
    channel_messages }o--|| organizations : "belongs to"
    context_observations }o--|| organizations : "belongs to"
    context_summaries }o--|| organizations : "belongs to"
```

***

## 3. Ingestion Detail

### Message Flow

1. Bot receives every Slack channel message (not DMs)
2. Fires `POST /context/ingest` with `organizationId`, `channelId`, `channelName`, `platformUserId`, `messageTs`, `threadTs`, `text`
3. Message inserted into `channel_messages` with `processedAt = null`
4. URLs extracted from message text via regex, cleaned of Slack formatting (`<url|display>` -> `url`)
5. Each URL queued as `process-url` BullMQ job -> Jina Reader API fetches content -> Claude Haiku summarizes -> stored as observation

### Batch Triggers

| Trigger      | Condition                     | Mechanism                                                     |
| ------------ | ----------------------------- | ------------------------------------------------------------- |
| Volume-based | `>= 20` unprocessed messages  | Immediate `process-batch` BullMQ job                          |
| Timer-based  | 30 minutes since last message | Delayed BullMQ job (deterministic jobId per org, no stacking) |

### Extraction Pipeline

1. Fetch unprocessed messages (limit `BATCH_SIZE`, default 20)
2. Resolve `platformUserId` -> user names via `platform_identities` + `users` tables
3. Group messages by channel, format with timestamps and display names
4. Send to **Claude Haiku 4.5** with extraction prompt
5. Parse JSON array of `{ entityType, entityId, category, observation, confidence }`
6. For each observation: generate embedding via OpenAI, insert into `context_observations`
7. Mark source messages as processed (`processedAt = now`)
8. Evaluate proactive actions via `context-actions.ts` rules

***

## 4. Query Flow — How the Bot Retrieves Context

```mermaid theme={null}
sequenceDiagram
    participant User
    participant ChatService
    participant ContextService
    participant EmbeddingsService
    participant Postgres as Postgres (pgvector)

    User->>ChatService: sends message
    ChatService->>ContextService: getRelevantContext(orgId, userId, message)

    ContextService->>Postgres: SELECT context_summaries<br/>WHERE entity_type='user' AND entity_id=userId<br/>AND summary_type='profile'
    Postgres-->>ContextService: user profile summary

    ContextService->>EmbeddingsService: generateEmbedding(message)
    EmbeddingsService-->>ContextService: 1536-dim vector

    ContextService->>Postgres: cosine similarity search on<br/>context_observations.embedding<br/>LIMIT 10
    Postgres-->>ContextService: top 10 relevant observations

    ContextService-->>ChatService: context string (max 2000 chars)<br/>= profile + top 5 observations

    ChatService->>ChatService: inject context into<br/>LLM system prompt
    ChatService->>User: contextually-aware response
```

### Context String Format

```
User profile: **Role & Responsibilities**: ... **Current Focus**: ... **Patterns**: ...

Relevant observations:
- [commitment] Committed to delivering the client report by Friday EOD (2026-03-28)
- [blocker] Blocked on design assets from external vendor (2026-03-27)
- [handoff] Took over newsletter formatting from Rohan (2026-03-26)
```

Capped at \~2000 characters (\~500 tokens) to avoid bloating the LLM prompt.

***

## 5. Entity Types and Observation Categories

### Entity Types

```mermaid theme={null}
mindmap
  root((Context<br/>Memory))
    user
      Preferences & work style
      Current tasks & focus
      Recurring patterns
      Handoff partners
      Mood signals
      Commitments & deadlines
    client
      Project requirements
      Communication history
      Key contacts
      Risk signals
    channel
      Purpose & topic
      Activity patterns
      Recurring discussions
    url
      Referenced resources
      Summarized content
    search
      Web search results
      External references
```

### Observation Categories

| Category           | Description                         | Confidence Threshold | Proactive Action           |
| ------------------ | ----------------------------------- | -------------------- | -------------------------- |
| `commitment`       | User promised to deliver something  | > 0.85               | Auto-create task           |
| `blocker`          | User is blocked on something        | > 0.7                | Create blocker record      |
| `handoff`          | Work transferred between people     | > 0.75               | DM receiving user          |
| `deadline_mention` | Deadline referenced in conversation | > 0.8                | Alert ops channel          |
| `risk_signal`      | Missed deadlines, scope changes     | > 0.7                | Create blocker, notify ops |
| `mood_signal`      | Frustration, burnout, excitement    | > 0.7                | Flag to ops channel        |
| `status_update`    | Substantive progress update         | > 0.5                | None                       |
| `reference`        | URL content or web search result    | 0.7-0.8              | None                       |

### Confidence Calibration

* **0.9+**: Directly stated fact ("I finished the newsletter")
* **0.7-0.9**: Strong inference ("Rohan always does QC after content — likely a handoff pattern")
* **0.5-0.7**: Weak inference (tone-based mood signals)
* **Below 0.5**: Not stored

***

## 6. Nightly Summarization

```mermaid theme={null}
flowchart LR
    subgraph "11 PM Cron: context-daily-summary"
        A[Get all unique<br/>entity pairs] --> B[For each entity:<br/>fetch last 50 observations]
        B --> C[Deduplicate<br/>Jaccard > 80% or substring]
        C --> D[Replace Slack IDs<br/>with display names]
        D --> E[Claude Haiku 4.5<br/>Synthesize profile]
        E --> F[Generate embedding]
        F --> G[Upsert into<br/>context_summaries]
    end

    subgraph "Profile Sections"
        S1[Role & Responsibilities]
        S2[Current Focus]
        S3[Patterns]
        S4[Risks]
        S5[Key Relationships]
    end

    G --> S1
    G --> S2
    G --> S3
    G --> S4
    G --> S5
```

### Summary Storage

* `summaryType = "profile"` — one per entity, upserted nightly
* Unique index on `(entity_type, entity_id, summary_type)` — ensures one profile per entity
* Embedding generated for the summary text itself (enables semantic search on summaries)
* Old summaries are overwritten, not versioned

***

## 7. Cleanup & Retention

```mermaid theme={null}
flowchart LR
    subgraph "2 AM Cron: context-cleanup"
        A[Delete from channel_messages<br/>WHERE created_at < NOW - retention_days]
    end

    CM[(channel_messages)] -->|pruned| A
    OBS[(context_observations)] -.->|kept indefinitely| OBS
    SUM[(context_summaries)] -.->|kept indefinitely| SUM
```

| Table                  | Retention                                              | Rationale                                 |
| ---------------------- | ------------------------------------------------------ | ----------------------------------------- |
| `channel_messages`     | 7 days (configurable via `CONTEXT_RAW_RETENTION_DAYS`) | Temporary buffer, facts already extracted |
| `context_observations` | Indefinite (soft-delete via `superseded_by`)           | Core knowledge graph                      |
| `context_summaries`    | Indefinite (overwritten nightly)                       | Entity profiles, always current           |

***

## 8. Backfill Architecture

For new orgs or channels, historical messages can be backfilled from Slack:

```mermaid theme={null}
flowchart TD
    TRIGGER[POST /context/backfill<br/>scope: org / channel] --> INIT[backfill-init job]
    INIT -->|fetch channel list via Slack API| STATE[Create backfill_state rows<br/>per channel]
    STATE --> CHANNEL[backfill-channel job<br/>process ONE page of ONE channel]
    CHANNEL -->|has more pages| CHANNEL
    CHANNEL -->|channel complete| NEXT[Next channel with<br/>status = pending]
    NEXT --> CHANNEL
    NEXT -->|all channels done| EXTRACT[backfill-extract job]
    EXTRACT -->|batch loop| HAIKU[Claude Haiku extraction<br/>same pipeline as real-time]
    HAIKU --> OBS[(context_observations)]

    CHANNEL -->|rate limited 429| WAIT[Wait 10s, retry]
    WAIT --> CHANNEL
    CHANNEL -->|not_in_channel| SKIP[Mark skipped]
```

* Fetches last 1 year of history
* 2-second delay between Slack API calls (rate limit safe)
* Thread replies fetched for threaded messages
* Uses `backfill_state` table to track progress per channel

***

## 9. Auto Mood Detection

Context observations with `category = "mood_signal"` feed into automatic mood scoring:

```mermaid theme={null}
flowchart LR
    OBS[(context_observations<br/>category = mood_signal)] --> CHECK{User has manual<br/>mood entry today?}
    CHECK -->|yes| SKIP[Skip]
    CHECK -->|no| HAIKU[Claude Haiku<br/>Score 1-5 from signals]
    HAIKU --> MOOD[(mood_entries<br/>note: 'Auto-detected')]
```

***

## 10. API Endpoints

| Method   | Endpoint                                         | Auth          | Purpose                                     |
| -------- | ------------------------------------------------ | ------------- | ------------------------------------------- |
| `POST`   | `/context/ingest`                                | API Key       | Ingest a channel message                    |
| `POST`   | `/context/browse`                                | API Key       | Fetch + summarize a URL                     |
| `POST`   | `/context/search`                                | API Key       | Web search + store results                  |
| `GET`    | `/context/stats`                                 | JWT / API Key | Dashboard stats (counts, categories)        |
| `GET`    | `/context/observations`                          | JWT / API Key | List observations (filterable)              |
| `GET`    | `/context/summaries`                             | JWT / API Key | List entity summaries                       |
| `GET`    | `/context/:entityType/:entityId`                 | JWT / API Key | Get entity context (summary + observations) |
| `GET`    | `/context/entity/:entityType/:entityId/timeline` | JWT / API Key | Full entity timeline                        |
| `POST`   | `/context/observations`                          | JWT / API Key | Manually create observation                 |
| `DELETE` | `/context/observations/:id`                      | JWT / API Key | Delete observation                          |
| `POST`   | `/context/observations/:id/supersede`            | JWT / API Key | Soft-delete (supersede)                     |
| `POST`   | `/context/backfill`                              | JWT / API Key | Trigger historical backfill                 |
| `GET`    | `/context/backfill/status`                       | JWT / API Key | Check backfill progress                     |

***

## 11. Environment Variables

| Variable                     | Required | Default | Purpose                                       |
| ---------------------------- | -------- | ------- | --------------------------------------------- |
| `ANTHROPIC_API_KEY`          | Yes      | —       | Claude Haiku for extraction & summarization   |
| `OPENAI_API_KEY`             | Yes      | —       | `text-embedding-3-small` for 1536-dim vectors |
| `JINA_API_KEY`               | No       | —       | URL content fetching via Jina Reader API      |
| `TAVILY_API_KEY`             | No       | —       | Web search via Tavily                         |
| `CONTEXT_BATCH_SIZE`         | No       | `20`    | Messages per extraction batch                 |
| `CONTEXT_RAW_RETENTION_DAYS` | No       | `7`     | Days to keep raw `channel_messages`           |

***

## 12. Key Files

| File                                                         | Purpose                                                   |
| ------------------------------------------------------------ | --------------------------------------------------------- |
| `packages/db/src/schema/channel-messages.ts`                 | Raw message buffer schema                                 |
| `packages/db/src/schema/context-observations.ts`             | Extracted facts + embeddings schema                       |
| `packages/db/src/schema/context-summaries.ts`                | Entity profile summaries schema                           |
| `apps/api/src/modules/context/context.service.ts`            | Core service: ingest, query, summarize, cleanup           |
| `apps/api/src/modules/context/context-processor.service.ts`  | BullMQ worker: batch extraction, URL processing, backfill |
| `apps/api/src/modules/context/context-embeddings.service.ts` | OpenAI embedding generation + pgvector similarity search  |
| `apps/api/src/modules/context/context-web.service.ts`        | Jina URL fetch + Tavily web search                        |
| `apps/api/src/modules/context/context-actions.service.ts`    | Proactive action evaluation                               |
| `apps/api/src/modules/context/context-backfill.service.ts`   | Backfill status tracking                                  |
| `apps/api/src/modules/context/context.controller.ts`         | REST API endpoints                                        |
| `packages/rules/src/context-actions.ts`                      | Rule engine: observation -> proactive action mapping      |
| `apps/api/src/modules/jobs/dynamic-cron.service.ts`          | Cron registration for daily summary + cleanup             |

***

## 13. Database Indexes

```sql theme={null}
-- channel_messages
CREATE INDEX channel_messages_org_processed_idx ON channel_messages(organization_id, processed_at);
CREATE UNIQUE INDEX channel_messages_org_channel_ts_idx ON channel_messages(organization_id, channel_id, message_ts);

-- context_observations
CREATE INDEX context_obs_entity_idx ON context_observations(entity_type, entity_id, category);
CREATE INDEX context_obs_org_entity_idx ON context_observations(organization_id, entity_type);
CREATE INDEX context_obs_expires_idx ON context_observations(expires_at);
-- HNSW index on embedding column for cosine similarity search

-- context_summaries
CREATE UNIQUE INDEX context_summaries_entity_type_idx ON context_summaries(entity_type, entity_id, summary_type);
-- HNSW index on embedding column
```

Requires `pgvector/pgvector:pg16` Docker image and `CREATE EXTENSION vector` migration.
