# Engram Daemon Architecture

**Version:** 0.2.0
**Last Updated:** 2026-02-01
**Schema:** v2 (proj_tag, ttl_tier, expires_at, content_hash)

This document provides a comprehensive dependency map of the claude-engram daemon architecture,
documenting all cross-module relationships, action handlers, and caller chains.

---

## Table of Contents

1. [Function-Level Call Graph](#1-function-level-call-graph)
2. [Action Type Catalog](#2-action-type-catalog)
3. [Dead Code Analysis](#3-dead-code-analysis)
4. [Communication Protocols](#4-communication-protocols)
5. [Key Architectural Patterns](#5-key-architectural-patterns)

---

## 1. Function-Level Call Graph

### 1.1 MemoryDaemon Public Methods

The `MemoryDaemon` class (daemon.py) exposes these public methods:

```
MemoryDaemon
├── __init__()
│   └── Called by: daemon.py::main() [daemon.py:402]
│   └── Initializes: MemoryStorage, QueryCache, checkpoint state
├── start()
│   └── Called by: asyncio.run() in daemon.py::main() [daemon.py:404]
│   └── Creates: Unix socket server, signal handlers, periodic cleanup task
│   └── Waits on: self._shutdown.wait()
├── stop()
│   └── Called by: signal handlers (SIGTERM, SIGINT) [daemon.py:341]
│   └── Cleanup: stops server, closes storage, removes socket/PID files
├── handle_client(reader, writer)
│   └── Called by: asyncio.start_unix_server() in start() [daemon.py:326]
│   └── Protocol: Read JSON line → _process_request() → Write JSON line
├── _process_request(request)
│   └── Called by: handle_client() [daemon.py:69]
│   └── Routes: action string → handler logic
│   └── See Section 2 for complete action catalog
└── _periodic_cleanup()
    └── Called by: asyncio.create_task() in start() [daemon.py:336]
    └── Calls: self.storage.cleanup_expired() every 3600s
```

#### Caller Chain for `_process_request()`

All external callers send JSON requests through Unix socket:

**1. CLI (cli.py)**
```
cli.py::send_command() [cli.py:33]
├── socket.socket(AF_UNIX, SOCK_STREAM)
├── client.connect(SOCKET_PATH)
├── client.sendall(json.dumps(request) + b'\n')
└── response = json.loads(client.recv())

Used by CLI commands:
├── cmd_store() → {"action": "store", "content": ..., "category": ...}
├── cmd_search() → {"action": "search", "query": ..., "limit": ...}
├── cmd_recent() → {"action": "recent", "limit": ...}
├── cmd_delete() → {"action": "delete", "id": ...}
├── cmd_stats() → {"action": "stats"}
└── cmd_status() → {"action": "ping"}
```

**2. MCP Server (mcp_server.py)**
```
mcp_server.py::_send_to_daemon() [mcp_server.py:59]
├── asyncio.open_unix_connection(socket_path)
├── writer.write(json.dumps(payload) + b'\n')
└── data = await reader.readline()

Used by MCP tools:
├── @mcp.tool() search_memories() → {"action": "search", ...}
├── @mcp.tool() store_memory() → {"action": "store", ...}
├── @mcp.tool() recent_memories() → {"action": "recent", ...}
└── @mcp.tool() delete_memory() → {"action": "delete", ...}
```

**3. Star Memory Bridge (star/memory/bridge.py)**
```
bridge.py::MemoryBridge._query() [bridge.py:40]
├── socket.socket(AF_UNIX, SOCK_STREAM)
├── sock.connect(self.socket_path)
├── sock.sendall(json.dumps(request) + b'\n')
└── response = json.loads(buf.split(b'\n')[0])

Used by Bridge public methods:
├── store() → {"action": "store", "content": ..., "category": ..., "proj_tag": ...}
├── search() → {"action": "search", "query": ..., "limit": ..., "proj_tag": ...}
├── get_recent() → {"action": "recent", "limit": ..., "proj_tag": ...}
├── batch_store() → {"action": "batch_store", "items": [...]}
├── batch_search() → {"action": "batch_search", "queries": [...]}
├── checkpoint_tick() → {"action": "checkpoint_tick", "session_id": ..., "profile": ...}
├── contextual_load() → {"action": "contextual_load", "message": ..., "proj_tag": ...}
├── inject() → {"action": "inject", "query": ..., "limit": ...}
└── is_available() → {"action": "ping"}
```

**4. Star Lifecycle Handlers (star/memory/lifecycle.py)**

Indirect via MemoryBridge methods:

```
handle_session_start() [lifecycle.py:27]
├── bridge.search("session checkpoint state", limit=1, category="checkpoint")
└── bridge.search("project decisions context", limit=3)

handle_notification() [lifecycle.py:94]
├── bridge.contextual_load(message, limit=3|5)
└── bridge._query({"action": "session_stats", "session_id": ...})

handle_post_tool_use() [lifecycle.py:157]
├── bridge.store(content=f"File modified: {path}", category="file_ownership")
└── bridge.checkpoint_tick(session_id, profile, proj_tag)

handle_stop() [lifecycle.py:208]
├── bridge.store(content=summary, category="session_summary", proj_tag=proj_tag)
└── bridge.batch_store([{lesson items}, {decision items}])
```

**5. JavaScript Hooks (star/hooks/*.js)**

All hooks use Node's `net.createConnection()`:

```
inject.js::queryDaemon() [inject.js:19]
├── Used for: SessionStart hook
└── Actions: get_config, star_startup, inject, what_changed

post-tool-use.js::queryDaemon() [post-tool-use.js:23]
├── Used for: PostToolUse hook
└── Actions: get_config, store (file_ownership), checkpoint_tick

save.js::storeToDaemon() [save.js:50]
├── Used for: Stop hook
└── Actions: store (session_summary), get_config

notification.js::queryDaemon() [notification.js:24]
├── Used for: Notification hook
└── Actions: get_config, contextual_load, session_stats
```

**6. Star Startup (star/startup.py)**

Special case - called FROM daemon, not to daemon:

```
daemon._process_request({"action": "star_startup"}) [daemon.py:162]
└── Imports and calls: claude_star.startup.run_startup()
    └── Returns: JSON hook output with plugin detection + profile
    └── Side effect: Updates daemon._last_startup_config cache
```

#### Internal Method Call Chains

```
_process_request(request)
├── action="store" → self.storage.store() [storage.py:98]
├── action="search" → self.storage.search() [storage.py:215]
├── action="recent" → self.storage.get_recent() [storage.py:268]
├── action="delete" → self.storage.delete() [storage.py:324]
├── action="stats" → self.storage.stats() [storage.py:419]
├── action="batch_store" → self.storage.batch_store() [storage.py:365]
├── action="batch_search" → self.storage.batch_search() [storage.py:391]
├── action="inject" → self.storage.search() [storage.py:215]
├── action="contextual_load"
│   ├── self.cache.get_exact(query) [cache.py:156]
│   ├── self.storage.search() [storage.py:215]
│   ├── self.cache.record_miss() [cache.py:107]
│   ├── self.cache.record_semantic_hit() [cache.py:115]
│   └── self.cache.put(query, response) [cache.py:136]
├── action="cache_stats" → self.cache.stats [cache.py:200]
├── action="checkpoint_tick"
│   └── Conditionally: self.storage.store() [storage.py:98]
├── action="star_startup" → claude_star.startup.run_startup() [startup.py:308]
├── action="what_changed" → self.storage.get_recent(since=last_end) [storage.py:268]
└── action="ping" → (no storage call)
```

### 1.2 MemoryStorage Public Methods

The `MemoryStorage` class (storage.py) exposes these public methods:

```
MemoryStorage
├── __init__(db_path)
│   └── Called by: MemoryDaemon.__init__(), test fixtures
│   └── Creates: SQLite connection (lazy), initializes schema
├── store(content, category, metadata, proj_tag) → int
│   └── Called by:
│       - daemon._process_request() [action=store, daemon.py:94]
│       - daemon._process_request() [action=checkpoint_tick, daemon.py:208]
│       - storage.batch_store() [storage.py:382]
│   └── Generates: embedding via _embed_text(content)
│   └── Returns: memory_id (deduped on content_hash + category)
├── search(query, limit, category, proj_tag, include_global) → list[dict]
│   └── Called by:
│       - daemon._process_request() [action=search, daemon.py:108]
│       - daemon._process_request() [action=inject, daemon.py:143]
│       - daemon._process_request() [action=contextual_load, daemon.py:234]
│       - storage.batch_search() [storage.py:409]
│   └── Generates: query embedding via _embed_text(query)
│   └── Uses: vec_distance_cosine(v.embedding, ?) for ranking
├── get_recent(limit, category, proj_tag, include_global, since) → list[dict]
│   └── Called by:
│       - daemon._process_request() [action=recent, daemon.py:119]
│       - daemon._process_request() [action=what_changed, daemon.py:290]
│   └── Sorts: by created_at DESC (chronological, no embedding)
├── delete(memory_id) → bool
│   └── Called by: daemon._process_request() [action=delete, daemon.py:128]
│   └── Cascades: deletes from memory_vectors + memories
├── stats() → dict
│   └── Called by: daemon._process_request() [action=stats, daemon.py:132]
│   └── Returns: total_memories, categories, db_path, schema_version, expired_count
├── cleanup_expired() → int
│   └── Called by: daemon._periodic_cleanup() [daemon.py:351]
│   └── Deletes: memories where expires_at < now()
├── batch_store(items) → list[int]
│   └── Called by: daemon._process_request() [action=batch_store, daemon.py:187]
│   └── Loops: calls store() for each item
├── batch_search(queries) → list[list[dict]]
│   └── Called by: daemon._process_request() [action=batch_search, daemon.py:192]
│   └── Loops: calls search() for each query
└── close()
    └── Called by: daemon.stop() [daemon.py:370]
    └── Closes: SQLite connection
```

#### Private Helper Methods

```
_get_conn() → sqlite3.Connection [storage.py:57]
├── Called by: _init_db(), store(), search(), get_recent(), delete(), cleanup_expired(), stats()
└── Lazy connection: loads sqlite-vec extension on first access

_init_db() [storage.py:73]
├── Called by: __init__()
└── Creates: memories table + memory_vectors vec0 table
└── Runs: migration.migrate_schema() for v2 schema

_build_where_clause(category, proj_tag, include_global) → (str, list) [storage.py:176]
├── Called by: search(), get_recent()
└── Builds: dynamic WHERE clause with expiration filter
└── Always adds: (expires_at IS NULL OR expires_at > ?)
```

---

## 2. Action Type Catalog

The daemon handles 16 distinct actions via `_process_request()`. Each action is described below
with parameters, return structure, and all external callers.

### Action: `store`

**Handler:** `daemon.py:91-105`
**Description:** Store a single memory with semantic embedding
**Required Parameters:**
- `content` (str): Memory text content

**Optional Parameters:**
- `category` (str): Memory category, default "general"
- `metadata` (dict): Extra metadata
- `proj_tag` (str): Project scope tag

**Returns:**
```json
{"id": <int>, "status": "stored"}
```

**Callers:**
- CLI: `cmd_store()`
- MCP: `store_memory()`
- Bridge: `MemoryBridge.store()`
- Lifecycle: `handle_post_tool_use()`, `handle_stop()`
- Hooks: `post-tool-use.js`, `save.js`
- Tests: `test_daemon_extended.py`, `test_storage_extended.py`

**Side Effects:**
- If `category="session_summary"` and `proj_tag` is set: updates
  `self._last_session_end[proj_tag]` timestamp for cross-window tracking

---

### Action: `search`

**Handler:** `daemon.py:107-116`
**Description:** Semantic search with vector similarity
**Required Parameters:**
- `query` (str): Search query text

**Optional Parameters:**
- `limit` (int): Max results, default 5
- `category` (str): Category filter
- `proj_tag` (str): Project scope filter
- `include_global` (bool): Include global memories, default True

**Returns:**
```json
{
  "status": "ok",
  "results": [
    {
      "id": int,
      "content": str,
      "category": str,
      "metadata": dict,
      "created_at": str,
      "proj_tag": str,
      "similarity": float
    }
  ]
}
```

**Callers:**
- CLI: `cmd_search()`
- MCP: `search_memories()`
- Bridge: `MemoryBridge.search()`
- Lifecycle: `handle_session_start()`
- Tests: `test_daemon_extended.py`, `test_storage_extended.py`

---

### Action: `recent`

**Handler:** `daemon.py:118-125`
**Description:** Get recent memories by creation time
**Required Parameters:** None

**Optional Parameters:**
- `limit` (int): Max results, default 10
- `category` (str): Category filter
- `proj_tag` (str): Project scope filter
- `include_global` (bool): Include global memories, default True

**Returns:**
```json
{
  "status": "ok",
  "results": [
    {
      "id": int,
      "content": str,
      "category": str,
      "metadata": dict,
      "created_at": str,
      "proj_tag": str
    }
  ]
}
```

**Callers:**
- CLI: `cmd_recent()`
- MCP: `recent_memories()`
- Bridge: `MemoryBridge.get_recent()`
- Tests: `test_cli.py`, `test_daemon_extended.py`

---

### Action: `delete`

**Handler:** `daemon.py:127-129`
**Description:** Delete a memory by ID
**Required Parameters:**
- `id` (int): Memory ID to delete

**Returns:**
```json
{"status": "ok", "deleted": bool}
```

**Callers:**
- CLI: `cmd_delete()`
- MCP: `delete_memory()`
- Tests: `test_daemon_extended.py`, `test_cli.py`

---

### Action: `stats`

**Handler:** `daemon.py:131-132`
**Description:** Get storage statistics
**Required Parameters:** None
**Returns:**
```json
{
  "status": "ok",
  "total_memories": int,
  "categories": {"category": count, ...},
  "db_path": str,
  "schema_version": int,
  "expired_count": int
}
```

**Callers:**
- CLI: `cmd_stats()`
- Tests: `test_daemon_extended.py`

---

### Action: `ping`

**Handler:** `daemon.py:134-135`
**Description:** Health check / daemon availability test
**Required Parameters:** None
**Returns:**
```json
{"status": "pong", "version": "0.2.0"}
```

**Callers:**
- CLI: `cmd_status()`
- Bridge: `MemoryBridge.is_available()`
- Tests: `test_daemon_extended.py`, `test_cli.py`

---

### Action: `inject`

**Handler:** `daemon.py:137-160`
**Description:** SessionStart context injection - formatted memory search
**Required Parameters:** None

**Optional Parameters:**
- `query` (str): Search query, default "recent decisions and context"
- `limit` (int): Max results, default 5
- `proj_tag` (str): Project scope filter

**Returns:**
```json
{"context": str, "count": int}
```

Context format:
```
## Recalled Memories

- [category] content
- [category] content
```

**Callers:**
- Bridge: `MemoryBridge.inject()`
- Hooks: `inject.js`
- Tests: `test_daemon_extended.py`

**Note:** Legacy action predating `contextual_load`. Still used by inject.js for SessionStart.

---

### Action: `star_startup`

**Handler:** `daemon.py:162-183`
**Description:** Star plugin startup - detect plugins and return hook JSON
**Required Parameters:** None
**Returns:**
```json
{"hook_output": dict}
```
or (on error):
```json
{"hook_output": {}, "warning": str}
{"hook_output": {}, "error": str}
```

**Callers:**
- Hooks: `inject.js`
- Tests: None (integration tested via hooks)

**Implementation:**
- Imports: `from claude_star.startup import run_startup`
- Calls: `run_startup()` which runs plugin detection pipeline
- Caches: `hook_output["profile"]` → `self._last_startup_config["profile"]`
- Caches: `hook_output["proj_tag"]` → `self._last_startup_config["proj_tag"]`

**Side Effects:**
- Updates `self._last_startup_config` cache for `get_config` action
- Sets `os.environ["STAR_PROJECT_TAG"]` (via startup.py)

---

### Action: `batch_store`

**Handler:** `daemon.py:185-188`
**Description:** Store multiple memories in one request
**Required Parameters:**
- `items` (list[dict]): List of memory dicts

Each item dict:
```json
{
  "content": str,  // required
  "category": str,  // optional
  "metadata": dict,  // optional
  "proj_tag": str   // optional
}
```

**Returns:**
```json
{"ids": [int, ...], "status": "stored", "count": int}
```

**Callers:**
- Bridge: `MemoryBridge.batch_store()`
- Lifecycle: `handle_stop()`
- Tests: `test_daemon_extended.py`

---

### Action: `batch_search`

**Handler:** `daemon.py:190-193`
**Description:** Run multiple semantic searches in one request
**Required Parameters:**
- `queries` (list[dict]): List of query dicts

Each query dict:
```json
{
  "query": str,  // required
  "limit": int,  // optional, default 5
  "category": str,  // optional
  "proj_tag": str,  // optional
  "include_global": bool  // optional, default True
}
```

**Returns:**
```json
{"results": [[...], [...]], "count": int}
```

**Callers:**
- Bridge: `MemoryBridge.batch_search()`
- Tests: `test_daemon_extended.py`

**Note:** Currently unused by production code but available for future bulk operations.

---

### Action: `checkpoint_tick`

**Handler:** `daemon.py:195-217`
**Description:** Increment checkpoint counter; auto-checkpoint based on profile threshold
**Required Parameters:** None

**Optional Parameters:**
- `session_id` (str): Session identifier, default "default"
- `profile` (str): Profile level (light/balanced/ultra), default "balanced"
- `proj_tag` (str): Project scope
- `snapshot` (str): Checkpoint snapshot content (~200 tokens)

**Returns:**
```json
{
  "checkpoint_fired": bool,
  "counter": int,
  "threshold": int
}
```

**Thresholds:**
- `light`: 40 tool calls
- `balanced`: 25 tool calls
- `ultra`: 15 tool calls

**Callers:**
- Bridge: `MemoryBridge.checkpoint_tick()`
- Lifecycle: `handle_post_tool_use()`
- Hooks: `post-tool-use.js`
- Tests: `test_daemon_extended.py`

**Side Effects:**
- Increments `self._checkpoint_counters[session_id]`
- When threshold reached: stores checkpoint memory and resets counter to 0

---

### Action: `contextual_load`

**Handler:** `daemon.py:219-263`
**Description:** Load contextually relevant memories for a message (Notification hook)
**Required Parameters:**
- `message` or `query` (str): User message text

**Optional Parameters:**
- `proj_tag` (str): Project scope
- `limit` (int): Max results, default 3
- `category` (str): Category filter

**Returns:**
```json
{"context": str, "count": int}
```

Context format (newline-separated):
```
[category] content
[category] content
```

**Callers:**
- Bridge: `MemoryBridge.contextual_load()`
- Lifecycle: `handle_notification()`
- Hooks: `notification.js`
- Tests: `test_daemon_extended.py`

**Caching Behavior:**
- Tier 1: Exact cache lookup via `cache.get_exact(query)`
- Tier 2: Semantic search via storage + adaptive threshold filtering
- Cache updates: `cache.put()` on semantic hits, `cache.record_miss()` on misses

---

### Action: `cache_stats`

**Handler:** `daemon.py:265-266`
**Description:** Get query cache statistics
**Required Parameters:** None
**Returns:**
```json
{
  "cache": {
    "hits": int,
    "misses": int,
    "hit_rate": float,
    "exact_hits": int,
    "semantic_hits": int,
    "similarity_threshold": float
  }
}
```

**Callers:**
- Tests: `test_daemon_extended.py`

**Note:** Diagnostic/monitoring endpoint. Not used by production code.

---

### Action: `session_stats`

**Handler:** `daemon.py:268-271`
**Description:** Get session checkpoint counter statistics
**Required Parameters:** None

**Optional Parameters:**
- `session_id` (str): Session identifier, default "default"

**Returns:**
```json
{"session_id": str, "tool_call_count": int}
```

**Callers:**
- Bridge: `MemoryBridge._query()` (direct)
- Lifecycle: `handle_notification()`
- Hooks: `notification.js`
- Tests: `test_daemon_extended.py`

**Use Case:** Check if compact recommendation should be shown (threshold: 80 tool calls).

---

### Action: `get_config`

**Handler:** `daemon.py:273-278`
**Description:** Get cached startup config (profile and proj_tag)
**Required Parameters:** None
**Returns:**
```json
{
  "status": "ok",
  "profile": str,
  "proj_tag": str | null
}
```

**Callers:**
- Hooks: `inject.js`, `post-tool-use.js`, `save.js`, `notification.js`
- Tests: `test_daemon_extended.py`

**Caching Behavior:**
- Values cached in `self._last_startup_config` by `star_startup` action
- Default values: `{"profile": "balanced", "proj_tag": null}`

---

### Action: `what_changed`

**Handler:** `daemon.py:280-307`
**Description:** Cross-window awareness - memories created since last session end
**Required Parameters:**
- `proj_tag` (str): Project scope (required, error if missing)

**Returns:**
```json
{
  "status": "ok",
  "count": int,
  "categories": {"category": count, ...}
}
```
or (on error):
```json
{"status": "error", "error": "proj_tag is required"}
```

**Callers:**
- Hooks: `inject.js`
- Tests: `test_daemon_extended.py`, `test_cli.py`

**Implementation:**
1. Retrieves `last_end = self._last_session_end.get(proj_tag)`
2. SQL-level time filtering: `get_recent(since=last_end, include_global=False)`
3. Python-level category filtering: only counts `CROSS_WINDOW_CATEGORIES`

**Side Effects:**
- Relies on `store` action with `category="session_summary"` to update timestamp

---

### Action: (unknown)

**Handler:** `daemon.py:309-310`
**Description:** Error handler for unknown actions
**Returns:**
```json
{"status": "error", "error": "Unknown action: <action>"}
```

**Callers:**
- Tests: `test_daemon_extended.py` (with `action="nonexistent_action"`)

---

## 3. Dead Code Analysis

### 3.1 Potentially Unused Functions

After analyzing all callers across the codebase, **zero public methods are unused**. All public
methods in both `MemoryDaemon` and `MemoryStorage` have legitimate callers.

### 3.2 Action Handlers Analysis

All 16 daemon actions have external callers:

| Action | External Callers | Used in Production | Notes |
|--------|-----------------|-------------------|-------|
| `store` | CLI, MCP, Bridge, Lifecycle, Hooks | ✅ | Core operation |
| `search` | CLI, MCP, Bridge, Lifecycle | ✅ | Core operation |
| `recent` | CLI, MCP, Bridge | ✅ | Core operation |
| `delete` | CLI, MCP | ✅ | Core operation |
| `stats` | CLI | ✅ | Diagnostic |
| `ping` | CLI, Bridge | ✅ | Health check |
| `inject` | Bridge, Hooks | ✅ | SessionStart legacy |
| `star_startup` | Hooks | ✅ | Plugin detection |
| `batch_store` | Bridge, Lifecycle | ✅ | Batch optimization |
| `batch_search` | Bridge | ⚠️ | Available but unused |
| `checkpoint_tick` | Bridge, Lifecycle, Hooks | ✅ | Auto-checkpoint |
| `contextual_load` | Bridge, Lifecycle, Hooks | ✅ | Post-compact recovery |
| `cache_stats` | Tests only | ⚠️ | Diagnostic only |
| `session_stats` | Bridge, Lifecycle, Hooks | ✅ | Compact recommendation |
| `get_config` | Hooks | ✅ | Profile resolution |
| `what_changed` | Hooks | ✅ | Cross-window awareness |

**Findings:**
1. **`cache_stats`**: Only used in tests. Diagnostic endpoint for cache behavior debugging.
   - **Recommendation:** Keep for debugging; consider adding to CLI as `claude-engram cache-stats`

2. **`batch_search`**: Implemented for API completeness but has zero production callers.
   - **Recommendation:** Keep; may be used by future bulk operations or MCP batch tools

### 3.3 Private Methods Analysis

All private/internal methods are actively used:

- `MemoryDaemon._periodic_cleanup()` → called by asyncio.create_task in start()
- `MemoryStorage._get_conn()` → called by all storage methods
- `MemoryStorage._init_db()` → called by __init__
- `MemoryStorage._build_where_clause()` → called by search() and get_recent()

**No dead private code detected.**

### 3.4 Helper Functions Analysis

All module-level helpers in storage.py are actively used:

- `_get_embedder()` → called by _embed_text()
- `_embed_text()` → called by store(), search()
- `_serialize_f32()` → called by store(), search()

**No dead helper code detected.**

---

## 4. Communication Protocols

### 4.1 Unix Socket Protocol

**Location:** `/tmp/claude-engram.sock` (configurable via `CLAUDE_ENGRAM_SOCKET` env var)
**Permissions:** 0600 (user read/write only)
**Format:** Line-delimited JSON

**Request:**
```json
{"action": "search", "query": "recent decisions", "limit": 5, "proj_tag": "proj:my-repo"}
```

**Response:**
```json
{"status": "ok", "results": [...]}
```

### 4.2 Client Implementations

| Client | Protocol | Timeout | Error Handling |
|--------|----------|---------|----------------|
| **CLI** (cli.py) | Sync: `socket.socket(AF_UNIX)` | 10s | Exit with error message |
| **MCP server** (mcp_server.py) | Async: `asyncio.open_unix_connection()` | Default | Return error string to user |
| **Bridge** (bridge.py) | Sync: `socket.socket(AF_UNIX)` | 2s | Return sensible defaults |
| **Hooks** (*.js) | Async: `net.createConnection()` | 2-5s | Fail silently (never block) |

### 4.3 Error Handling Policy

**Daemon-side:**
- All exceptions caught in `_process_request()`
- Returns: `{"status": "error", "error": str(e)}`

**Client-side:**
- **CLI:** Exits with status 1 on timeout or error
- **MCP:** Returns user-friendly error strings
- **Bridge:** Returns sensible defaults (None, empty list, empty dict)
- **Hooks:** Fail silently (return empty JSON), never block Claude Code

---

## 5. Key Architectural Patterns

### 5.1 Error-Safe by Design

All public APIs in `bridge.py` and `lifecycle.py` are error-safe:
- Never raise exceptions to callers
- Return sensible defaults (empty lists, None, False)
- Log warnings but continue execution

### 5.2 Profile-Aware Behavior

Three profiles (LIGHT, BALANCED, ULTRA) control memory overhead:

| Feature | LIGHT | BALANCED | ULTRA |
|---------|-------|----------|-------|
| file_ownership tracking | ❌ Skip | ✅ Store | ✅ Store |
| Notification contextual load | ❌ Skip | ✅ 3 results | ✅ 5 results |
| Checkpoint frequency | 40 tool calls | 25 tool calls | 15 tool calls |

Profile is resolved at `star_startup` and cached in daemon state for JS hooks.

### 5.3 Cross-Window Awareness

`what_changed` action tracks memories stored by other Claude sessions:
- Daemon maintains `_last_session_end` dict (proj_tag → ISO timestamp)
- Stop hook stores `session_summary` category memory (triggers timestamp update)
- SessionStart hook queries `what_changed` to show "X decisions, Y lessons stored by other
  sessions"
- Only `CROSS_WINDOW_CATEGORIES` are reported (lesson, decision, interface)

### 5.4 Deduplication Strategy

Content-based deduplication at store time:
- `content_hash = sha256(content.encode()).hexdigest()`
- Unique constraint: `(content_hash, category)`
- On collision: return existing memory ID (idempotent stores)

### 5.5 TTL Tiers

Three-tier expiration system:

| Tier | Categories | TTL | Use Case |
|------|-----------|-----|----------|
| **permanent** | lesson | None | Cross-project wisdom |
| **medium** | decision, interface, file_ownership, session_summary | 7 days | Project context |
| **short** | checkpoint | 4 hours | Compact recovery |

Expired memories are cleaned up by `_periodic_cleanup()` every hour.

### 5.6 Caching Strategy

QueryCache (cache.py) provides two-tier caching for `contextual_load`:
1. **Exact match cache**: Hash-based lookup for identical queries
2. **Adaptive threshold**: Adjusts similarity threshold based on hit/miss rate
3. **Cache warming**: Semantic hits populate cache for future exact matches

---

## Cross-Module Dependency Summary

```
External Callers → Unix Socket → MemoryDaemon → MemoryStorage → SQLite-vec
                                             └→ QueryCache

CLI (cli.py)
├── send_command() → socket → _process_request()
└── Actions: store, search, recent, delete, stats, ping

MCP Server (mcp_server.py)
├── _send_to_daemon() → socket → _process_request()
└── Actions: store, search, recent, delete

Star Memory Bridge (star/memory/bridge.py)
├── _query() → socket → _process_request()
└── Actions: store, search, recent, batch_store, batch_search, checkpoint_tick,
    contextual_load, inject, ping, session_stats

Star Lifecycle Handlers (star/memory/lifecycle.py)
├── handle_session_start() → bridge.search()
├── handle_notification() → bridge.contextual_load(), bridge._query(session_stats)
├── handle_post_tool_use() → bridge.store(), bridge.checkpoint_tick()
└── handle_stop() → bridge.store(), bridge.batch_store()

JavaScript Hooks (star/hooks/*.js)
├── inject.js → Actions: get_config, star_startup, inject, what_changed
├── post-tool-use.js → Actions: get_config, store, checkpoint_tick
├── save.js → Actions: store, get_config
└── notification.js → Actions: get_config, contextual_load, session_stats

Star Startup (star/startup.py)
└── run_startup() ← Called via star_startup action (import-based, not socket)
```

---

## File References

### Daemon Core

- `packages/engram/src/claude_engram/daemon.py` - MemoryDaemon class, main()
- `packages/engram/src/claude_engram/storage.py` - MemoryStorage class
- `packages/engram/src/claude_engram/cli.py` - CLI commands
- `packages/engram/src/claude_engram/mcp_server.py` - FastMCP bridge
- `packages/engram/src/claude_engram/cache.py` - QueryCache
- `packages/engram/src/claude_engram/categories.py` - MemoryCategory enum, TTL definitions
- `packages/engram/src/claude_engram/project.py` - derive_project_tag()
- `packages/engram/src/claude_engram/migration.py` - Schema migration

### Star Integration

- `packages/star/hooks/inject.js` - SessionStart hook
- `packages/star/hooks/save.js` - Stop hook
- `packages/star/hooks/notification.js` - Notification hook
- `packages/star/hooks/post-tool-use.js` - PostToolUse hook
- `packages/star/src/claude_star/memory/bridge.py` - MemoryBridge class
- `packages/star/src/claude_star/memory/lifecycle.py` - handle_* functions
- `packages/star/src/claude_star/startup.py` - run_startup() orchestrator

### Tests

- `packages/engram/tests/test_daemon_extended.py` - Action tests
- `packages/engram/tests/test_cli.py` - CLI command tests
- `packages/engram/tests/test_mcp_server.py` - MCP tool tests
- `packages/engram/tests/test_storage_extended.py` - Storage tests
- `packages/star/tests/test_bridge.py` - Bridge API tests

---

## Version History

- **v0.2.0** (Phase 3.7): Added star_startup, checkpoint_tick, contextual_load, session_stats,
  get_config, what_changed, inject, batch operations, cache_stats. Cross-window awareness.
  Profile-aware behavior. TTL tiers.
- **v0.1.0** (Phase 3.6): Initial MCP server, basic actions (store, search, recent, delete, stats,
  ping)
