docs(rfc-001): add Log concept, append-only storage architecture, workflow event sourcing
- §2.4: Log as data asset, not trigger source (anti-avalanche constraint) - §3: Add Log to terminology table - §5.4: New storage architecture section - Unified logs table (append-only SQLite) - Workflow state via event sourcing (no mutable tables) - Cold archival: >30d data exported to daily JSONL files - §5.6: Error handling now writes logs instead of error signals - §8: Directory structure updated with logs.db and archive/ - §10: Design principles updated (8 principles, +1 log rule) - Thread outputs are now Logs, not Signals 小橘 <xiaoju@shazhou.work>
This commit is contained in:
@@ -29,6 +29,26 @@
|
||||
|
||||
一个 Sense **怎么算**(compute)和**什么时候算**(reflex)是两个独立的关注点。同一个 Sense 可以被定时触发、被事件触发、被按需查询触发。
|
||||
|
||||
### 2.4 Log 是数据资产,不是触发源
|
||||
|
||||
系统运行过程中产生的各种记录(reflex 执行记录、workflow 状态变迁、错误日志等)统称为 **Log**。Log 是有价值的数据资产,用于审计、溯源、分析。
|
||||
|
||||
**Log 与 Signal 的本质区别:**
|
||||
|
||||
- **Signal** — 来自外部世界的感知数据,可以触发 Reflex
|
||||
- **Log** — 系统内部产出的副作用记录,**不能触发 Reflex**
|
||||
|
||||
因果链是单向的:
|
||||
|
||||
```
|
||||
外部世界 → Sense → Signal → Reflex → Action + Log
|
||||
↑
|
||||
Reflex 可以读 Log(查询/聚合)
|
||||
Log 不能触发 Reflex ✗
|
||||
```
|
||||
|
||||
禁止 Log 触发 Reflex 是防止雪崩的关键约束——如果 reflex 执行产生的 log 又能触发 reflex,系统会形成无限循环。Log 是因果链的终点,不是起点。
|
||||
|
||||
## 3. 术语表
|
||||
|
||||
| 术语 | 隐喻 | 含义 |
|
||||
@@ -36,6 +56,7 @@
|
||||
| **Sense** | 感官 | 定义怎么感知——compute 函数,每种 Sense 有自己的数据类型和独立存储 |
|
||||
| **Reflex** | 反射 | 定义什么时候感知——声明式触发条件 |
|
||||
| **Signal** | 信号 | Sense compute 返回非 null 时发出的通知,其他 Reflex 可以监听 |
|
||||
| **Log** | 日志 | 系统内部产出的记录(执行记录、状态变迁、错误等),数据资产,不触发 Reflex |
|
||||
| **Workflow** | 行动 | 定义怎么做——内含 Moderator(调度)和 Role(执行)|
|
||||
| **Moderator** | 协调者 | Workflow 内部概念,在 Role 之间递话筒 |
|
||||
| **Role** | 执行者 | Workflow 内部概念,执行具体动作,有副作用 |
|
||||
@@ -298,13 +319,13 @@ Signal 和 Thread 是两个独立的循环,单向桥接:
|
||||
Signal 循环 ──→ ThreadStart ──→ Thread 循环
|
||||
(无状态,幂等,可合并) (有状态,顺序,Command Event 驱动)
|
||||
│
|
||||
Thread 产出 Signal
|
||||
Thread 产出 Log
|
||||
(执行日志,供 retrospection)
|
||||
```
|
||||
|
||||
**Signal 只负责 kickoff Thread**。Thread 启动后,由自己的事件循环驱动——Moderator 递话筒、Role 执行、Command Event 流转。Thread 内部不走 Signal 系统。
|
||||
|
||||
**Thread 产出的 Signal 是执行日志**,记录 Thread 的中间状态和最终结果。这些 Signal 可以被其他 Sense 监听用于 retrospection(如统计成功率、平均耗时),但不作为驱动 Workflow 的动力。
|
||||
**Thread 产出的 Log 是执行日志**,记录 Thread 的中间状态和最终结果。这些 Log 可以被 Sense 的 compute 查询用于 retrospection(如统计成功率、平均耗时),但 Log 不能触发 Reflex(见 §2.4)。
|
||||
|
||||
这保证了两个循环的性质不被污染:
|
||||
- Signal 循环:无状态、幂等、可合并、可丢弃
|
||||
@@ -437,50 +458,86 @@ Sense 的运行时属性(`group`、`throttle`、`timeout`)在 `nerve.yaml`
|
||||
- **timeout**:compute 超时上限(soft timeout),超时后 abort 当前 compute,记录错误 signal
|
||||
- **grace_period**:soft timeout 后的宽限期(默认 timeout × 3),超过后 hard kill 整个 group worker 并 respawn。防止跑飞的 compute 堵住同 group
|
||||
|
||||
### 5.4 Thread 状态持久化与恢复
|
||||
### 5.4 存储架构
|
||||
|
||||
Thread 是状态机。每一步转换之前持久化状态,确保崩溃后可恢复。
|
||||
系统有两大类持久化数据,全部 append-only:
|
||||
|
||||
```typescript
|
||||
interface ThreadState {
|
||||
threadId: string
|
||||
workflowId: string
|
||||
currentStep: string // 状态机当前节点
|
||||
context: Record<string, any> // 累积的上下文
|
||||
history: CommandEvent[] // 已完成的步骤记录
|
||||
status: 'running' | 'crashed' | 'completed' | 'failed'
|
||||
updatedAt: number
|
||||
}
|
||||
#### Signal 存储
|
||||
|
||||
每个 Sense 独立一个 SQLite 文件(见 §8),由 Sense 自行管理 schema。这部分不变。
|
||||
|
||||
#### Log 存储
|
||||
|
||||
所有 Log 写入统一的 SQLite 文件 `data/logs.db`,单表:
|
||||
|
||||
```sql
|
||||
CREATE TABLE logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
source TEXT NOT NULL, -- "reflex", "workflow", "system"
|
||||
type TEXT NOT NULL, -- "run_start", "run_complete", "error", "state_change"
|
||||
ref_id TEXT, -- 关联的 reflex name / workflow run_id
|
||||
payload TEXT, -- JSON
|
||||
ts INTEGER NOT NULL -- unix ms
|
||||
);
|
||||
|
||||
CREATE INDEX idx_logs_source_type ON logs(source, type);
|
||||
CREATE INDEX idx_logs_ts ON logs(ts);
|
||||
CREATE INDEX idx_logs_ref_id ON logs(ref_id);
|
||||
```
|
||||
|
||||
执行流程:
|
||||
- **统一一张表**,通过 `source + type` 区分 log 来源和类型
|
||||
- Reflex 可以查询 logs 表(只读),但 log 不能触发 reflex(见 §2.4)
|
||||
|
||||
```
|
||||
moderator 决定下一步 → 持久化状态 → execute role → 写结果 → 下一步
|
||||
│
|
||||
如果这里挂了
|
||||
│
|
||||
恢复时从这一步重试
|
||||
#### Workflow 状态:事件溯源
|
||||
|
||||
Workflow Thread 的状态不用 mutable 表,而是 append-only 的事件流:
|
||||
|
||||
```sql
|
||||
-- 也在 logs 表中,source = "workflow"
|
||||
-- type 取值:queued, started, step_complete, completed, failed, crashed
|
||||
```
|
||||
|
||||
恢复流程:
|
||||
当前状态 = 该 run_id 最后一条 log entry。例:
|
||||
|
||||
```
|
||||
1. engine 检测到 workflow worker 挂了
|
||||
2. respawn worker
|
||||
3. worker 启动时扫描 db,找到 status=running 的 thread
|
||||
4. 从 currentStep 恢复执行
|
||||
source=workflow, type=queued, ref_id=run-7, ts=1000
|
||||
source=workflow, type=started, ref_id=run-7, ts=1001
|
||||
source=workflow, type=completed, ref_id=run-7, ts=1005
|
||||
```
|
||||
|
||||
恢复要求 Role 的 execute 尽量幂等。非幂等的 Role 可以标记:
|
||||
查询当前活跃 workflow:
|
||||
|
||||
```typescript
|
||||
roles: [
|
||||
{ id: 'analyzer', execute: ..., idempotent: true }, // 可安全重试
|
||||
{ id: 'deployer', execute: ..., idempotent: false }, // 崩溃后标记 crashed,等介入
|
||||
]
|
||||
```sql
|
||||
SELECT ref_id, type FROM logs
|
||||
WHERE source = 'workflow'
|
||||
AND (ref_id, ts) IN (
|
||||
SELECT ref_id, MAX(ts) FROM logs
|
||||
WHERE source = 'workflow'
|
||||
GROUP BY ref_id
|
||||
)
|
||||
AND type IN ('queued', 'started')
|
||||
```
|
||||
|
||||
进程重启时从 log 重建内存状态。运行时用内存 materialized view 加速查询。
|
||||
|
||||
#### 冷归档
|
||||
|
||||
Engine 定期(cron 或内置 task)将超过 30 天的 log 和 signal 数据导出为按天 JSONL 文件归档:
|
||||
|
||||
```
|
||||
data/
|
||||
logs.db # 热数据(近 30 天)
|
||||
archive/
|
||||
logs/
|
||||
2026-03-22.jsonl # 冷数据,按天归档
|
||||
2026-03-23.jsonl
|
||||
senses/
|
||||
cpu-usage/
|
||||
2026-03-22.jsonl
|
||||
```
|
||||
|
||||
导出后从主库 DELETE + VACUUM。冷数据用 grep/jq 即可查询,不需要 SQL。
|
||||
|
||||
### 5.5 热更新
|
||||
|
||||
主进程 watch `~/.uncaged-nerve/` 文件变化,按类型处理:
|
||||
@@ -507,9 +564,9 @@ nerve.yaml diff 处理:
|
||||
|
||||
| 情况 | 处理 |
|
||||
|------|------|
|
||||
| compute 抛异常 | 记录错误 signal,下次触发重试 |
|
||||
| compute 超时 | soft timeout → abort + 记录错误 signal;grace_period 后 hard kill worker + respawn |
|
||||
| 存储写入失败 | 记录错误,不发 signal(未成功产出) |
|
||||
| compute 抛异常 | 写 log(source=system, type=error),下次触发重试 |
|
||||
| compute 超时 | soft timeout → abort + 写 error log;grace_period 后 hard kill worker + respawn |
|
||||
| 存储写入失败 | 写 error log,不发 signal(未成功产出) |
|
||||
| nerve.yaml 语法错误 | daemon 拒绝加载,保持当前配置 |
|
||||
| sense ts 语法错误 | 该 group worker 加载失败,其他 group 正常 |
|
||||
| workflow worker 崩溃 | 幂等 thread 自动恢复,非幂等标记 crashed |
|
||||
@@ -629,10 +686,17 @@ Reflex ──→ Sense ──→ Sense (复合依赖)
|
||||
workflows/ # post-MVP
|
||||
cleanup.ts
|
||||
data/ # ⛔ gitignored
|
||||
logs.db # 统一 log 存储(append-only)
|
||||
senses/
|
||||
cpu-usage.db # 每个 sense 独立的 sqlite
|
||||
disk-usage.db
|
||||
active-tasks.db
|
||||
archive/ # 冷归档(>30天)
|
||||
logs/
|
||||
2026-03-22.jsonl
|
||||
senses/
|
||||
cpu-usage/
|
||||
2026-03-22.jsonl
|
||||
blobs/ # CAS blob store,sha256 寻址
|
||||
ab/
|
||||
cd1234...
|
||||
@@ -665,7 +729,8 @@ Reflex ──→ Sense ──→ Sense (复合依赖)
|
||||
1. **Sense 是唯一的一等公民** — 原始采样和派生计算统一为 Sense,Sense 不知道下游
|
||||
2. **计算与触发解耦** — compute 不知道自己什么时候被调用
|
||||
3. **Reflex 是 Event Mesh** — 所有事件路由都声明在 Reflex 中,Sense 和 Workflow 之间无直连
|
||||
4. **存储去中心化** — 每个 Sense 自管存储,没有统一 Event Store
|
||||
5. **不删除只关停** — 历史不可变,生命周期通过 enabled 控制
|
||||
6. **Workflow 是可选扩展** — MVP 不需要,后续按需加入
|
||||
7. **引擎极简** — 只做调度,不做业务逻辑
|
||||
4. **Log 是终点不是起点** — Log 是数据资产,Reflex 可读但不被 Log 触发,防止雪崩
|
||||
5. **存储去中心化** — 每个 Sense 自管存储,Log 统一一张表,全部 append-only
|
||||
6. **不删除只关停** — 历史不可变,生命周期通过 enabled 控制
|
||||
7. **Workflow 是可选扩展** — MVP 不需要,后续按需加入
|
||||
8. **引擎极简** — 只做调度,不做业务逻辑
|
||||
|
||||
Reference in New Issue
Block a user