Seed 播种声明(INV-SEED-01)
wesgine 中 CellSpec 的 Cron / Email 声明是"出生声明"——只在 Cell 首次 boot 播种,此后存储是唯一权威。
设计原则
CellSpec.Cron / CellSpec.Email
↓
首次 boot → 播种进 wes_cron_jobs / wes_email_configs
↓
此后存储是唯一权威,声明不再被读
原缺陷:两件外衣同一个 bug
Cron:删除复活
裸 INSERT 播种。用户运行时删掉的任务,下次 boot 复活——让 INSERT 冲突的那行没了。
"删除"静默地只等于"到重启为止"。
Email:密码被覆盖
upsert 播种。运行时改的密码被声明覆盖回去且哪里都不报错。
标记表 wes_spec_seed
CREATE TABLE IF NOT EXISTS wes_spec_seed (
kind TEXT PRIMARY KEY,
seeded_at TEXT NOT NULL
);
为什么同层
标记表与它守护的行同层(state.db)。
分层会让一次快照回滚把行退回而标记留在前面,Cell 从此"已播种但空"。
空声明也打标
标记在探测成功后无条件写,含声明为空。
按 len(spec)>0 加门会让没声明 cron 的 Cell 永不打标,几个月后编辑 CellSpec 就播种进用户手工维护的存储。
探测失败不写标记
把决定权留给下一次 boot。
IMAP 收信注册
IMAP 收信注册与温度 imapWarmnessCount 一律读存储:
// ✅ 读存储
accounts, _ := emailStore.ListAccounts(ctx)
// ❌ 读声明(会让 UI 新增的账号重启后收不到信)
accounts := spec.Email
仅探测失败时退回声明作估计(偏多 = 钉住 Warm,安全一侧)。
配额判点随之拆开
MaxCronJobs / MaxEmailAccounts 曾在 Cell.Start 拿 len(spec) 校验。
而声明首次 boot 后既不再被读、也没有 API 能改,于是两个方向都错:
| 方向 | 问题 |
|---|---|
| 声明 3 条而存储 50 条 | 照样通过 |
| 把任务全删光的 Cell | 永远起不来 |
现在:
| 时机 | 校验 |
|---|---|
| 创建时 | validateSeedDeclarationCaps 拒绝装不下自己的声明 |
| 运行时 | cap 落在唯一写入点 cron.Scheduler.Add / email.Store.PutAccount |
Channels / MCPs 对比
| 特性 | Cron / Email | Channels / MCPs |
|---|---|---|
| 有运行时存储? | ✅ | ❌ |
| 权威源 | 存储 | spec |
| 播种? | 是(一次性) | 否(spec 就是唯一副本) |
两种形状都是单一真相。坏的是第二份持久副本。
但 cap 不因此留在 Cell.Start——因为集合就是 spec,越界写入持久化成功后下次重启才拒绝启动,判点同样下沉到唯一增长点(INV-QUOTA-05)。
反模式
| 禁止 | 正确做法 |
|---|---|
| 每次 boot 无条件从声明插入 | 标记表控制,只播种一次 |
按 len(spec) > 0 控制是否打标 | 空声明也打标 |
| IMAP 注册读声明 | 读存储,失败退回声明估计 |
配额在 Cell.Start 校验 len(spec) | 创建时 validate + 运行时唯一写入点 |
| 标记表与数据分层 | 同层(state.db),防快照回滚裂脑 |