# Issue #100: 实时学习 — L0 步骤从 SessionEnd 拆到 observe 实时执行

- **GitHub**: https://github.com/gladego/index1-dev/issues/100
- **状态**: ✅ 完成
- **分支**: dev
- **关联**: maintenance.py 8 步管道、hooks.py observe、#83 reflect 绑定

---

## 1. 问题背景

当前维护管道（maintenance.py）在 SessionEnd 触发，8 步串行执行，150s 超时。三个问题：

1. **SessionEnd 有已知 bug 可能不触发** — awaken 有兜底，但知识更新延迟到下次 session
2. **L0 步骤不需要等 SessionEnd** — 衰减、因果推断、promote 检查、GC 都是纯规则，可以实时执行
3. **observe 已是异步** — 有足够预算做额外 L0 工作（~3-5ms 额外开销）

## 2. 开源调研

### 2.1 对标项目实时维护策略

| 项目 | 策略 | 关键设计 | 批处理频率 |
|------|------|---------|-----------|
| **Mem0** | per-turn extraction + async consolidation | 每次对话立即提取事实，后台异步合并去重 | 每次交互 |
| **Zep/Graphiti** | <250ms incremental processing | 增量处理，每次交互后立即更新知识图谱 | 每次交互 |
| **Letta/MemGPT** | sleep-time agent (every N steps) | 后台 agent 按计数器周期触发维护 | 每 N 步 |
| **LlamaIndex** | token-ratio based | 按 token 消耗比例触发压缩/合并 | 按 token 比例 |
| **Cognee** | 3-stage pipeline | 提取→聚合→检索，每阶段可独立触发 | 按阶段 |

### 2.2 行业共识

**即时提取 + 异步聚合。批处理-only 已过时。**

核心模式：
- 轻量操作（规则推断、衰减、GC）→ 实时/增量执行
- 重量操作（embedding、LLM 合并、摘要生成）→ 异步批处理
- 两者通过标记/队列协调，避免重复执行

### 2.3 index1 策略选择

**计数器周期调度**（类似 Letta sleep-time agent）：
- L0 步骤在 observe 实时执行（纯规则，无 LLM 依赖）
- L1/L2 步骤保留 SessionEnd 批处理（需要 embedding/chat 模型）
- 通过 marker 文件协调两端，避免重复执行

选择理由：
- 最符合 index1 的三层降级架构（L0/L1/L2）
- observe 是 async 后台，无延迟约束
- marker 文件机制简单可靠，无需引入消息队列

## 3. 当前维护管道分析

### 3.1 维护 8 步分层分类

| 步骤 | 层级 | 需 LLM | 可实时 | 复杂度 | 决策 |
|------|------|--------|--------|--------|------|
| decay | L0 | ❌ | ✅ | 纯 SQL | → **实时（周期 50 次）** |
| consolidate | L1 | ✅ embed | ❌ | backfill + vec search | 保留批处理 |
| infer_relations | L0+L2 | 部分 | L0 可 | L0 规则 + L2 episode | **L0→实时，L2→批处理** |
| update_models | L0+L2 | 部分 | 部分 | auto_create + chat | 保留批处理（复杂） |
| extract_tactics | L0 | ❌ | ✅ | SQL 聚合 | → **实时（周期 20 次）** |
| promote_universal | L0 | ❌ | ✅ | 规则检查 | → **实时（per-access）+ 批处理兜底** |
| generate_brief | L1 | ✅ | ❌ | chat + 模板 | 保留批处理 |
| gc | L0 | ❌ | ✅ | 纯 SQL | → **实时（周期 100 次）** |

### 3.2 实时化候选（4 个 L0 步骤）

| 步骤 | 触发方式 | 频率 | 预估耗时 |
|------|---------|------|---------|
| 因果推断 L0 | `on_fact_saved()` | 每个 fact | ~2ms |
| promote 检查 | `on_fact_saved()` | 每个 fact | ~1ms |
| decay | `periodic_l0()` | 每 50 次 observe | ~5ms |
| extract_tactics | `periodic_l0()` | 每 20 次 observe | ~10ms |
| gc | `periodic_l0()` | 每 100 次 observe | ~5ms |

## 4. 架构设计

### 4.1 核心组件：RealtimeUpdater

```
新文件: src/index1/cognitive/realtime.py

RealtimeUpdater
├── on_fact_saved(fact, collection, session_id)
│   ├── _infer_edges_incremental()  # 增量因果推断（最近 5 个 facts）
│   └── _check_promote()            # promote_to_universal 检查
├── periodic_l0(collection, session_id)
│   ├── 每 50 次 → decay_facts()
│   ├── 每 20 次 → _extract_tactics_l0()
│   └── 每 100 次 → garbage_collect()
└── marker 管理
    ├── write_marker()    # observe 写入
    ├── clear_marker()    # reflect 清除
    └── check_marker()    # maintenance 检查
```

### 4.2 共享规则函数

从 maintenance.py 提取到 realtime.py 顶层，两个模块共用：

```python
# realtime.py — 共享的规则函数
def looks_project_specific(assertion: str) -> bool
def scope_overlap(fact_a: dict, fact_b: dict) -> bool
def match_edge_rule(prev: dict, curr: dict) -> tuple[str, float] | None
def match_triple_rule(prev2, prev, curr) -> list[tuple] | None
```

### 4.3 协调机制：Marker 文件

```
observe 每次写入:
  → on_fact_saved()          # 即时 L0
  → periodic_l0()            # 周期 L0
  → 写 .realtime_active_{collection} marker

SessionEnd maintenance:
  → check_marker() → 跳过已实时化的 L0 步骤
  → 只执行: consolidate, infer_relations(L2), update_models, promote(兜底), generate_brief
  → 最后 clear_marker()

Marker 过期: >15 分钟自动清除（防止 orphan marker）
```

### 4.4 hooks.py 集成

```python
# HookHandler.__init__ 新增:
self._realtime = RealtimeUpdater(db_cog, config)

# observe() fact 写入后追加:
if self._realtime:
    self._realtime.on_fact_saved(fact, collection, session_id)

# observe() 返回前追加:
if self._realtime:
    self._realtime.periodic_l0(collection, session_id)

# reflect() 中清除 marker:
if self._realtime:
    RealtimeUpdater.clear_marker(collection)
```

## 5. 降级矩阵

| 场景 | L0 实时 | L1/L2 批处理 | 结果 |
|------|--------|-------------|------|
| 正常 | ✅ observe 时执行 | ✅ SessionEnd 执行 | 最优 |
| observe 熔断 | ❌ 跳过 | ✅ SessionEnd 兜底全部 8 步 | 退回原行为 |
| SessionEnd 不触发 | ✅ 已实时完成 | ❌ awaken 兜底 | L0 不丢失 |
| DB 不可写 | ❌ safe_db_write 返回 None | ❌ safe_db_write 返回 None | 全部跳过 |
| realtime.py import 失败 | ❌ observe 降级为原行为 | ✅ 不受影响 | 退回原行为 |

## 6. 性能预算

| 操作 | 耗时 | 频率 |
|------|------|------|
| `on_fact_saved` 因果推断 | ~2ms | 每个 fact |
| `on_fact_saved` promote 检查 | ~1ms | 每个 fact |
| `periodic_l0` decay | ~5ms | 每 50 次 observe |
| `periodic_l0` extract_tactics | ~10ms | 每 20 次 observe |
| `periodic_l0` gc | ~5ms | 每 100 次 observe |
| marker 文件写入 | ~1ms | 每次 periodic |
| **总额外开销 per-observe** | **~3-5ms** | |

observe 是 async 后台，无延迟预算限制，5ms 额外开销完全可接受。

## 7. 任务清单

| # | 任务 | 状态 | 文件 | 预估行数 |
|---|------|------|------|---------|
| 1 | 新建 `realtime.py` + 提取共享函数 | ✅ 完成 | `cognitive/realtime.py`(新建) + `cognitive/maintenance.py`(修改) | ~430+40 |
| 2 | hooks.py observe() 集成 RealtimeUpdater | ✅ 完成 | `cognitive/hooks.py` | ~28 |
| 3 | maintenance.py 感知 marker + 跳过 L0 | ✅ 完成 | `cognitive/maintenance.py` | ~30 |
| 4 | 测试 + 验证（723 tests passing） | ✅ 完成 | `tests/test_realtime.py`(新建) + `tests/test_hooks.py` + `tests/test_maintenance.py` | ~290+100+92 |
| 5 | 代码审查（0 P0, 4 P1→3 已修复） + 文档更新 | ✅ 完成 | `.claude/CLAUDE.md` | ~10 |

## 8. 设计决策

| 决策 | 选择 | 原因 |
|------|------|------|
| 实时调度方式 | 计数器周期（类 Letta） | 简单可靠，无需引入定时器/队列 |
| 协调机制 | marker 文件 | 轻量，跨进程可见，无需 IPC |
| 共享代码方式 | 提取到 realtime.py 顶层函数 | maintenance.py import 使用，避免循环依赖 |
| promote 保留批处理兜底 | 是 | realtime 只做 per-access 检查，批处理做全量扫描兜底 |
| marker 过期时间 | 15 分钟（code review 后从 1h 调整） | 防止 orphan marker 永久阻止 maintenance L0 |
| 因果推断模式 | 增量（最近 5 个 facts） | 避免全量扫描开销，适合实时场景 |

## 9. 文件清单

| 文件 | 操作 | 预估行数 |
|------|------|---------|
| `src/index1/cognitive/realtime.py` | 新建 | ~380 |
| `src/index1/cognitive/maintenance.py` | 修改 | ~40 |
| `src/index1/cognitive/hooks.py` | 修改 | ~25 |
| `tests/test_realtime.py` | 新建 | ~200 |
| `tests/test_hooks.py` | 修改 | ~40 |
| `tests/test_maintenance.py` | 修改 | ~30 |
| `.claude/CLAUDE.md` | 修改 | ~10 |
| **合计** | | **~725** |
