# DuckDB Rebuild Timeout — Investigation for Axiom Developer

## Symptom

`axiom_semantic_index rebuild rebuild_mode="full" use_duckdb=true` consistently times out via MCP transport regardless of timeout value.

## Environment

- Binary: `/home/busya/dev/axiom-mcp-rust-port/target/release/axiom-mcp-server-rs`
- Workspace: `/home/busya/dev/ss-tools` (3185 contracts, 2149 edges, 690 files)
- DuckDB path: `.axiom/semantic_index/graph.duckdb`
- Expected DuckDB size: ~225 MB (final, from earlier successful rebuild)
- OS: Linux, 64-bit

## Investigation Log

### Attempt 1: Timeout scaling test (old binary)

| use_duckdb | Timeout | Result | Notes |
|:----------:|:-------:|:------:|-------|
| false | 120s | ✅ Success | 3185 contracts, 2149 edges |
| **true** | **120s** | ❌ **Timeout** | |
| **true** | **300s** | ❌ **Timeout** | |
| **true** | **600s** | ❌ **Timeout** | |
| **true** | **900s** | ❌ **Timeout** | |

### Attempt 2: After chunking fix + error logging (new binary)

| Step | Result | Notes |
|------|:------:|-------|
| `rebuild use_duckdb=true` | ❌ `"Failed to populate DuckDB store"` | **Timeout побеждён!** Код не зависает |
| Очистка DuckDB + retry | ❌ `"Failed to populate DuckDB store"` | Та же логическая ошибка |
| Удалён `index.duckdb` (394MB legacy) + retry | ❌ `"Failed to populate DuckDB store"` | Не помогло |
| `rebuild (no DuckDB)` + сразу `rebuild use_duckdb=true` | ❌ `"Failed to populate DuckDB store"` | Даже после успешного non-DuckDB rebuild |
| `rebuild use_duckdb=false` (контрольный) | ✅ **Always success** | 3185 contracts, 2149 edges |

**Итого:** `"Failed to populate DuckDB store"` воспроизводится всегда, стабильно, на чистой DuckDB. Ошибка в Rust-коде популяции, не в данных.

### Attempt 3: Clean database

1. Moved old `graph.duckdb` (225MB) → `graph.duckdb.bak`
2. Tried `rebuild use_duckdb=true` — still ❌ timeout
3. **Partial write observed**: new `graph.duckdb` grew 0→38MB before timeout
4. After retry: grew 38→63MB — then **stalled completely**
5. WAL file (`graph.duckdb.wal`) appeared and disappeared, suggesting incomplete transactions

### Attempt 4: Non-DuckDB works flawlessly

```
reindex  →  120s  ✅  (3185 contracts, 0 edges, instant)
rebuild (no DuckDB) → 300s ✅ (3185 contracts, 2149 edges)
audit_contracts after rebuild → responds instantly
```

## Key observations

1. **DuckDB write starts but never finishes**. First attempt wrote 38MB, second wrote 63MB, then stopped making progress. Expected final size is ~225MB.
2. **Non-DuckDB operations are fast**. The contract parsing, relation resolution, and JSON index writing all complete within 180-300s.
3. **MCP transport times out before DuckDB finishes**. The MCP protocol returns `-32001: Request timed out` at the transport layer. The server process may still be running but the client disconnected.
4. **DuckDB file is 63MB, WAL is empty**. The partial file suggests the DuckDB transaction started but never committed.
5. **DuckDB WAL appeared then disappeared** between attempts, suggesting a rollback occurred.

## Root cause found! Duplicate @RELATION edges in source code

После логирования удалось выявить точную причину: **PRIMARY KEY constraint violation** при вставке в таблицу edges. DuckDB использует первичный ключ `(source_id, relation_type, target_id)`, а в коде есть дублирующиеся `@RELATION` строки.

### Примеры дубликатов (подтверждено)

```
DatasetReviewEntryUxTests:DEPENDS_ON:DatasetReviewWorkspaceEntry  (2 файла, одинаковый contract_id)
BackupsPage:IMPLEMENTS:RoutePages                                   (2 файла, одинаковый contract_id)
DeleteRunningTasksUtil:DEPENDS_ON:TaskRecord                       (2 строки в одном файле)
AuthJwtModule:DEPENDS_ON:auth_config                               (2 строки в одном файле)
```

### Масштаб проблемы

Скрипт поиска (`grep -r @RELATION | sort | uniq -d`) показал **десятки файлов** с дубликатами: profile.py (7 копий), database.py (2), async_network.py (2), security.py (2), и много тестовых файлов.

### Варианты фикса

#### Вариант A: Rust-код (рекомендуется)

Изменить вставку в DuckDB с `INSERT` на `INSERT OR REPLACE` или `INSERT OR IGNORE`:

```rust
// Текущий код (падает при дубликатах):
conn.execute("INSERT INTO edges (source, type, target) VALUES (?, ?, ?)", ...)?;

// Фикс 1: INSERT OR IGNORE — пропускает дубликаты
conn.execute("INSERT OR IGNORE INTO edges (source, type, target) VALUES (?, ?, ?)", ...)?;

// Фикс 2: INSERT OR REPLACE — перезаписывает
conn.execute("INSERT OR REPLACE INTO edges (source, type, target) VALUES (?, ?, ?)", ...)?;
```

**Преимущество:** не требует правки кода на проекте, устойчив к дубликатам в будущем.

#### Вариант B: Правка кода (трудоёмко)

Найти и удалить все дублирующиеся `@RELATION` строки в проекте. Оценка: ~50 файлов, ~200 дубликатов.

### Текущий статус (после ручного фикса 4 дубликатов)

| Попытка | Результат |
|---------|:---------:|
| `rebuild use_duckdb=true` | ❌ Всё ещё падает — есть ещё дубликаты (всего ~50 файлов) |
| `rebuild use_duckdb=false` | ✅ Всегда работает (JSON index не имеет unique constraint) |

### Рекомендация

Разработчику Axiom: **реализовать Вариант A** (`INSERT OR IGNORE`). Это:
1. Решает проблему мгновенно
2. Не требует правки проекта
3. Устойчив к регрессиям
4. Не теряет данные — дубликаты @RELATION это одна и та же связь, записанная дважды

## Suggested fix approach

```rust
// Before (current — single large write blocks MCP):
pub async fn rebuild_with_duckdb() -> Result<()> {
    let contracts = parse_all_files()?;
    let conn = DuckDbConnection::open(path)?;
    conn.execute("BEGIN TRANSACTION")?;       // single giant tx
    for c in &contracts {
        conn.execute("INSERT INTO contracts ...", params!(c))?;
    }
    conn.execute("COMMIT")?;                   // may take minutes
    Ok(())
}

// After — chunked with MCP yield:
pub async fn rebuild_with_duckdb(stream: &McpStream) -> Result<()> {
    let contracts = parse_all_files()?;
    let conn = DuckDbConnection::open(path)?;
    // Smaller batches to prevent transport timeout
    for chunk in contracts.chunks(500) {
        conn.execute("BEGIN TRANSACTION")?;
        for c in chunk {
            conn.execute("INSERT INTO contracts ...", params!(c))?;
        }
        conn.execute("COMMIT")?;
        // Signal progress to keep MCP connection alive
        stream.send(Progress { done: chunk_end, total: contracts.len() }).await?;
        // Yield to allow other MCP requests
        tokio::task::yield_now().await;
    }
    Ok(())
}
```

## Workaround (for current version)

Until the DuckDB rebuild is fixed, use the two-step process:
1. `axiom_semantic_index reindex` (in-memory, instant)
2. `axiom_semantic_index rebuild rebuild_mode="full" use_duckdb=false` (JSON index, ~180-300s)

The JSON index at `.axiom/semantic_index/index.json` is fully functional for `search_contracts`, `read_outline`, and `local_context`. Only `audit_contracts` with DuckDB-dependent schema validation may show stale tag validation results.

## Files referenced

- DuckDB binary path: `.axiom/semantic_index/graph.duckdb` (63 MB partial)
- JSON index path: `.axiom/semantic_index/index.json` (fresh, 3185 contracts, 2149 edges)
- Old DuckDB backup: `.axiom/semantic_index/graph.duckdb.bak` (225 MB)
- Axiom config: `.axiom/axiom_config.yaml` (all tag/permission fixes applied)
