跳到主要内容

FinClaw V1 Data and Persistence Design

状态:Accepted Initial Design / B-2 工程蓝图 日期:2026-05-16 项目:FinClaw 文档级别:项目级数据 / 持久化基线 上游文档:v1-prd.md §5 / §6 / §10v1-product-object-and-schema-design.md(全部 §3 ~ §13)、v1-engineering-kickoff-decisions.md D-10 / D-12v1-commercial-signal-instrumentation-design.md §3 / §8 配套文档:v1-tech-stack-and-architecture-design.md(B-1)、v1-api-contract-design.md(B-3)

1. Purpose

本文回答 V1 工程仓库 /Users/mlabs/Programs/CurvatureLabs/finclaw/ 在数据持久化层的 4 个问题:

  1. 哪些对象需要持久化?分别由谁写、谁读?
  2. V1 阶段用什么存储后端?为什么不直接上 Postgres?
  3. 文件系统上的路径、ID、版本号、append-only / mutable 边界如何统一约定?
  4. 隐私 / 保留 / 删除如何在数据层落地(与 v1-engineering-kickoff-decisions.md D-12 对齐)?

本文重复 v1-product-object-and-schema-design.md 的字段定义;持久化只是在它之上加路径 / 版本 / 生命周期。

2. Persistence Targets

V1 持久化对象按数据分类:

2.1 一等认知对象(用户主消费)

ObjectSourceMutability主要消费者
MarketCognitionSnapshotObject Writer 写入append-only(一旦写入不修改)Snapshot View / Refresh diff baseline
MarketCognitionThreadObject Writer 写入;用户操作变更 status / 元数据mutable header + append-only historyThread View / Refresh diff
PreExecutionCheckpointObject Writer 写入append-onlyCheckpoint View / Thread reference

2.2 质量与证据支撑对象

ObjectSourceMutability主要消费者
EvidenceItemEvidence Checkerappend-only(每次任务新生成;用 claim_ref 关联 snapshot)Evidence Drawer
DataQualityNoteEvidence Checkerappend-onlyEvidence Drawer / UI badges
AdvisorOutputAdvisor Plannerappend-onlySnapshot 内 advisor_outputs 引用 / debug

2.3 用户上下文与边界对象

ObjectSourceMutability主要消费者
UserContextTask Router / Onboardingmutable(task-scoped 默认;保存到 profile 需 ProfileConsent)Context Engine
ProfileConsentProfileConsentDialog(v1-ui-ux-interaction-design.md §5 / §8mutable + version-trackedBoundary Guard / 全部敏感操作前置检查
SensitiveInputHandlingSensitive Input Classifier(v1-agent-orchestration-design.md §9append-onlyAudit / Evaluation reviewer
TrainingAssetCandidateObject Writer + Privacy guardmutable status;append-only metadata(V1 不进入训练;仅治理记录)

2.4 评测与轨迹对象

ObjectSourceMutability主要消费者
EvaluationCase仓库内 evaluation/cases/*.yaml(受 git 管理)mutable via PR;运行时只读Evaluation Runner
EvaluationRunEvaluation Runnerappend-onlyEvaluation Review packet / Trial closeout
TraceAgentRuntimeappend-only(一次任务一份)Debug / Eval reviewer / Postmortem

2.5 商业信号事件对象

ObjectSourceMutability主要消费者
CommercialSignalEventFrontend 上报 + Backend 触发点(v1-commercial-signal-instrumentation-design.md §4append-onlyWeekly cs report 脚本(v1-commercial-signal-instrumentation-design.md §7

2.6 显式不持久化对象

不持久化理由
凭证 / 私钥 / 助记词 / API key(v1-product-object-and-schema-design.md §9 credential 类)永不写盘;分类后立即拒收(仅 SensitiveInputHandling 记录中保留 classification + masked stub,不含明文)
用户原始 chat raw(包含可能的敏感金融上下文,未经分类)不写 raw chat log;只写经过分类与边界检查的 UserContext / Snapshot
LLM raw prompt / response 全文默认入 trace;只 trace 结构化字段 + token usage(如需 debug,dev 模式下可选 --full-trace,仅本机)
订单 / 仓位 / 钱包 / 链上交易状态FinClaw 边界外(mvp-product-definition.md §3

3. Storage Backend Choice

3.1 V1 默认:文件系统 + JSONL + Markdown

维度V1 选型理由
主存储POSIX 文件系统 (本机 / docker volume)Labs 内测 3 人,无需 RDB 并发能力
结构化对象JSON files (per object)易于 git diff、易于人工 review
时序事件JSONL files (*.jsonl)append-only 友好;流式读取
配置 / SkillYAML / Markdown与现有 SKILL.md 体系一致
内嵌索引SQLite(仅 commercial signal 报表 + Trial 期临时 query)v1-commercial-signal-instrumentation-design.md §9 已锁定 SQLite + Python 报表

3.2 不选 Postgres / 数据库的理由

V1 状态何时考虑 RDB
用户规模3 人内测trial 退出 / 公开扩展前(≥ 50 用户)触发 §10 迁移路径
写并发ReAct 同步、单进程多进程 / 多 worker 时
跨用户 query极少(cs report 用 SQLite 即可)有 admin dashboard 跨用户分析需求时
事务一致性单文件原子写(os.replace)足够涉及多对象事务时
全文 searchV1 不实现(thread 列表按 metadata sort 即可)用户希望全文检索 snapshot 时

3.3 SQLite 的有限使用

仅以下场景使用 SQLite,作为主存储:

用途文件写入者读取者
Commercial signal events 索引data/events/cs/index.sqliteevent_sink.py 旁路写入Weekly report 脚本
Trial 期 funnel / retention 查询同上同上同上
LLM telemetry 聚合data/eval/llm_telemetry.sqlitecost_telemetry.py 旁路写入make eval-cost-summary

主对象(Snapshot / Thread / Checkpoint / 等)进 SQLite。SQLite 文件可随时重建(从 JSONL replay)。

3.4 预留 RDB 接口

CognitionStore 在代码层抽象为 Protocol:

class CognitionStore(Protocol):
async def save_snapshot(self, snapshot: MarketCognitionSnapshot) -> None: ...
async def get_snapshot(self, snapshot_id: str) -> MarketCognitionSnapshot | None: ...
async def list_snapshots(self, user_id: str, *, limit: int = 20) -> list[SnapshotIndexEntry]: ...
# ... 其他对象类似

V1 实现:FileSystemCognitionStore。 V2 / 扩展时实现:PostgresCognitionStore,无需改 caller 代码。

4. Path Conventions

4.1 顶层布局(在 data/ 下)

data/
├── README.md ← 数据目录说明 + privacy 警告
├── .kill_switch ← 触发后 API 进入 503 模式(§8.5 / 8.5)

├── cognition/<user_id>/ ← 用户主认知数据
│ ├── snapshots/<snapshot_id>.json
│ ├── snapshots/_index.jsonl ← append-only 索引
│ ├── threads/<thread_id>.json ← thread header (mutable)
│ ├── threads/<thread_id>.history.jsonl ← refresh history (append-only)
│ ├── threads/_index.jsonl
│ ├── checkpoints/<checkpoint_id>.json
│ ├── checkpoints/_index.jsonl
│ ├── evidence/<evidence_id>.json
│ ├── evidence/_index.jsonl
│ ├── quality_notes/<quality_id>.json
│ ├── quality_notes/_index.jsonl
│ └── advisor_outputs/<advisor_output_id>.json

├── consents/<user_id>.yaml ← ProfileConsent (mutable, version-tracked)
├── consents/_history/<user_id>/<version>.yaml ← 历史版本

├── user_contexts/<user_id>/<context_id>.json

├── sensitive/<user_id>/<input_ref>.json ← SensitiveInputHandling (no payload, only metadata)

├── training_candidates/<user_id>/<candidate_id>.json

├── traces/<task_id>.jsonl ← per-task ReAct trace

├── events/
│ ├── cs/<YYYY-MM-DD>.jsonl ← Commercial signal events (per-day)
│ ├── cs/index.sqlite ← 旁路索引
│ └── system/<YYYY-MM-DD>.jsonl ← system events (kill switch, boundary block)

└── eval/
├── runs/<run_id>/
│ ├── manifest.yaml
│ ├── per_case/<case_id>.json
│ ├── llm_telemetry.jsonl
│ └── reviewer_notes.md
└── llm_telemetry.sqlite ← 聚合索引

4.2 Path 约定规则

规则描述
user_id 隔离任何 user-scoped 对象路径都包含 <user_id>/;跨用户访问由 API 层 auth 拦截
_index.jsonl每个对象目录都有 append-only 索引文件(一行一个对象的 minimal metadata:id / title / created_at / status)
_history/mutable 对象的历史版本归档目录
<YYYY-MM-DD>.jsonl时序事件按日切分;不按 user 切分(事件本身含 user_anon_id
文件名禁止用户输入snapshot_id 等 ID 由系统生成(§5);用户输入的 title 仅写入文件内容,不进文件名
大小写全部 lowercase + _ 分隔;不允许 path 大小写歧义

4.3 文件原子写

def atomic_write_json(path: Path, payload: dict) -> None:
tmp = path.with_suffix(path.suffix + ".tmp")
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2))
os.replace(tmp, path) # POSIX rename = atomic

JSONL 用 open(..., "a") 追加;single-process 假设保证不会出现 interleave。

4.4 引用约定

跨对象引用使用 <object_type>:<id> 形式的字符串(与 v1-api-contract-design.md §6 Object Reference Conventions 对齐):

字段
linked_snapshots[]["snapshot:snap_2026-05-18T10-23-04Z_a3f5b2"]
pre_execution_checkpoint_refs[]["checkpoint:chk_..."]
claim_ref"snapshot:snap_xxx#supporting_reasons[2]"(§6 路径表达)
previous_snapshot_ref"snapshot:snap_yyy"

CognitionStore 提供 resolve(ref: str) -> dict | None 方法做反查。

5. ID and Versioning

5.1 ID 命名规则

每个对象有 stable identifier,构造规则:

ObjectPrefix构造
Snapshotsnap_snap_<YYYY-MM-DDTHH-MM-SSZ>_<6-char-hash>snap_2026-05-18T10-23-04Z_a3f5b2
Threadthr_thr_<YYYY-MM-DDTHH-MM-SSZ>_<6-char-hash>thr_2026-05-18T10-30-00Z_b7c1d4
Checkpointchk_chk_...
Evidenceevi_evi_...
DataQualityNoteqln_qln_...
AdvisorOutputadv_adv_...
UserContextctx_ctx_...
ProfileConsentcsn_csn_<user_id>_v<version>csn_user_alice_v3
SensitiveInputHandlingsnh_同 snapshot patternsnh_...
TrainingAssetCandidatetac_tac_...
Tracetr_tr_<task_id>tr_task_...
CommercialSignalEventevt_UUIDv4evt_<uuid>
EvaluationRunrun_run_<YYYY-MM-DDTHH-MM-SSZ>_<4-char-hash>run_...
PersonaDriftLogdrift_同 snapshot patterndrift_2026-05-18T11-04-09Z_3e8a91B-6 §3.3 新引入对象)

<6-char-hash> 取自 secrets.token_hex(3),用于避免同秒冲突;ID 编码 user_id(user_id 在 path 中体现)。

5.2 Version 字段

mutable 对象(Thread / ProfileConsent / TrainingAssetCandidate)有 version 字段:

schema_version: "1.0" # 字段 schema 版本,与 server 代码一起 bump
object_version: 7 # 该对象本身的修改次数(每次 mutate +1)
updated_at: <iso8601>

Snapshot / Checkpoint / EvidenceItem 等 append-only 对象没有 object_version,但仍需 schema_version

5.3 schema_version 兼容策略

V1 期间预期 schema_version"1.0"。如发生 breaking change:

  • minor 增量(1.1):CognitionStore 加载时按字段缺省值兼容;
  • major 增量(2.0):写迁移脚本 server/scripts/migrate_v1_to_v2.py,落盘前备份 data/.backup-pre-migrate-<timestamp>/

5.4 时间字段约定

  • 全部时间字段使用 ISO-8601 + UTC(Z 后缀);
  • API 出口对前端时按用户 tz 转;存储统一 UTC;
  • created_at 写入时设置;之后修改;
  • updated_at mutable 对象每次写入更新;append-only 对象含此字段。

6. Refresh Semantics

V1 的 Thread refresh 是产品核心循环(v1-prd.md §6.5v1-product-object-and-schema-design.md §4.3)。持久化层契约:

6.1 Thread 头与历史的分离

文件内容Mutability
threads/<thread_id>.jsonThread header:当前 thesis / counter / watch_questions / refresh_conditions / invalidators / status / linked_snapshots[] / last_refreshed_atmutable(每次 refresh 更新)
threads/<thread_id>.history.jsonl一行一个 RefreshChangev1-product-object-and-schema-design.md §4.3append-only

6.2 Refresh 操作的精确语义

一次 thread refresh 完成时持久化层执行:

  1. 生成新 Snapshot(写入 snapshots/<snap_id>.json);
  2. threads/<thread_id>.history.jsonl 追加一条 RefreshChange(含 previous_snapshot_ref + new_snapshot_ref + diff 字段);
  3. 原子更新 threads/<thread_id>.json 头:
    • linked_snapshots[] 追加新 snap_id移除旧的);
    • current_thesis / counter_thesis / watch_questions / invalidators / evidence_state 替换为新值;
    • last_refreshed_at = now;
    • object_version += 1;
  4. 写入 traces/<task_id>.jsonl
  5. 推送 SSE event thread_refreshed

6.3 历史 Snapshot 不被覆盖(硬约束)

  • 任何 Thread mutation 不得修改或删除 linked_snapshots[] 中的历史 Snapshot
  • Snapshot 文件写入后永不修改;如发现错误,新建 corrected snapshot + 在 thread history 追加 correction_note,不修改原文件。

6.4 Refresh 一致性保证

  • Refresh 期间出错(步骤 1 ~ 4 任一失败):
    • 新 Snapshot 文件可能已写入(无副作用,可孤立存在);
    • Thread header 更新;
    • history.jsonl 追加;
    • 用户看到 SSE task_failed,下次 refresh 重新尝试;
  • 不引入跨文件事务(V1 用文件系统不要求 ACID)。

6.5 ProfileConsent 撤回的副作用

v1-engineering-kickoff-decisions.md D-12v1-product-object-and-schema-design.md §8.2

撤回 consent 时持久化层动作:

动作
ProfileConsentmutate:set revoked_at;写入 consents/<user_id>.yaml 新版本,旧版本归档到 _history/
已生成的 Snapshot / Thread / Checkpoint保留(不删除;用户仍可查看)
后续 LLM 调用UserContext 不再注入 saved profile 字段
Commercial signal events自撤回时刻起新写 events;历史 events 在 48h 内由 delete_user_events 脚本删除
TrainingAssetCandidate全部置 withdrawal_status: revoked;不进入任何后续训练流

7. Retention and Deletion Policy

7.1 Retention Matrix

ObjectDefault RetentionTrial 期Trial 退出后
Snapshot / Thread / Checkpoint永久(用户主动删除前)同;用户撤回 consent 不删除
EvidenceItem / DataQualityNote跟随对应 Snapshot
AdvisorOutput90 天,过期归档到 _archive/
UserContext (task-scoped)任务结束后 24 小时
UserContext (saved with consent)跟随 ProfileConsent
ProfileConsent永久(含 _history/永久(合规证据)
SensitiveInputHandling永久(仅 metadata,无明文)永久(合规证据)
TrainingAssetCandidate永久(受 ProfileConsent.training_use_allowed 约束)同;revoke 后 48h 内 purge
Trace90 天后归档到 traces/_archive/
CommercialSignalEvent90 天后归档;trial 退出 30 天内全部脱敏
EvaluationRun永久(受 git 管理 / 治理库副本)

7.2 删除分类

V1 区分 4 种"删除":

类型描述实现
user-soft-delete用户 UI 上"删除" Snapshot / Thread在 header 写 deleted_at;文件保留;列表过滤
user-hard-delete用户在 settings 触发 "delete account"python -m server.scripts.delete_user --user_id=X 物理删除该 user_id 下所有目录;ProfileConsent 保留删除日志 stub
consent-withdrawal用户撤回 consent,但保留账号§6.5
retention-expiry数据到期归档cron-less:python -m server.scripts.archive_expired 由人工定期运行(V1 不上 cron)

7.3 归档结构

data/cognition/<user_id>/snapshots/_archive/<YYYY-MM>/<snapshot_id>.json
data/traces/_archive/<YYYY-MM>/<task_id>.jsonl
data/events/cs/_archive/<YYYY-MM>.jsonl.gz

归档改变 ID;查询时若主目录未命中,自动 fallback 查 _archive/

7.4 Kill Switch 与 Retention 的关系

data/.kill_switch 触发不影响 retention:

  • 已写入数据保留;
  • 新写入暂停(API 拒绝);
  • Trial closeout decision = stop 时再走 user-hard-delete 流程。

8. Backup and Export

8.1 V1 备份策略(Labs 内测)

频率内容落点
每天 02:00(人工)data/ 全量 tar.gz本机 ~/finclaw-backups/<YYYY-MM-DD>.tgz,保留 14 天
每周(人工)同上移动到 Labs 团队加密存储(未定,待 trial 启动前与项目发起人对齐)
边界事件后立即data/events/system/ + 触发任务的 data/traces/~/finclaw-backups/incidents/<task_id>/

V1 实现自动备份(不上 cron);trial 期间由项目发起人手动触发 python -m server.scripts.backup

8.2 用户数据导出(GDPR-friendly 雏形)

python -m server.scripts.export_user_data --user_id=X --out=./out/

输出目录结构:

<out>/<user_id>/
├── README.md ← 描述本导出包内容
├── consents/ ← ProfileConsent 全部版本
├── cognition/ ← Snapshot / Thread / Checkpoint
├── evidence/ ← Evidence + DataQualityNote
├── events/ ← 该用户的 CommercialSignalEvent(脱敏后)
├── traces/ ← 该用户的 Trace(仅 task summary,不含 raw prompt)
└── manifest.yaml ← 文件清单 + checksum

V1 内测期间用户主动请求时手动跑该脚本;V2 暴露为 API endpoint。

8.3 导出包含

  • 凭证 / 私钥(永不入库);
  • 其他用户的数据(user_id 隔离);
  • LLM raw prompt / response(默认未持久化);
  • 内部 advisor reasoning(仅暴露最终 AdvisorOutput.key_points)。

9. Privacy Boundaries

9.1 与 v1-engineering-kickoff-decisions.md D-12 的对齐

V1 内测期间隐私 / 合规复核降紧迫不豁免。本文已在数据层先行落地以下约束:

约束落点
凭证 / 私钥永不写盘§2.6 显式不持久化对象
ProfileConsent 必须存证§4.1 consents/<user_id>.yaml + _history/ 永久保留
sensitive_input 仅写 metadatadata/sensitive/<user_id>/<input_ref>.jsonclassification / masked_stub含明文
training pipeline 受 consent 闸门TrainingAssetCandidate 落盘需 user_authorized: true + de_identified: true + sensitive_filtered: truev1-product-object-and-schema-design.md §11
trace 保留 90 天后归档§7.1
commercial signal 撤回后 48h 内删除§6.5

9.2 user_anon_id vs user_id

V1 区分两个标识:

标识出现位置关联 PII
user_iddata/cognition/<user_id>/...、ProfileConsent、UserContext关联 Labs 内测用户真实身份(仅在邀请码 → user_id 映射表中持有;表本身存 data/_internal/user_id_map.json,仅项目发起人可读)
user_anon_idCommercialSignalEvent payload、EvaluationRun reviewer notesuser_id 经 HMAC 派生(key 仅在 server config,不入库),仅项目发起人可逆查

凡跨用户聚合分析(cs report、eval reviewer notes)一律用 user_anon_id

9.3 数据分类标签

写入对象前必须打标签:

标签适用处理
cognition_outputSnapshot / Thread / Checkpoint / Evidence / Quality默认可保存;用户可删除
user_voluntary_contextUserContext / ProfileConsent需 ProfileConsent 才长期保存
sensitive_metadataSensitiveInputHandling永久保留 metadata(合规证据),明文不留
behavior_signalCommercialSignalEventconsent_for_trial_data 闸门
system_observabilityTrace / system events不含 PII;仅 task_id / structural fields

9.4 数据流出边界

流向允许?备注
→ 默认 LLM provider(GPT-5.5 / Kimi K2.6)仅当任务需要;遵守 provider TOS;不主动发送 saved profile 除非 ProfileConsent.save_to_thread
→ BYOM provider是(但受限)TrainingAssetCandidate 路径禁用 BYOM(v1-model-and-provider-policy.md §4
→ Labs 评测知识库 (projects/Labs-FinTecAI/evaluation/finclaw/runs/)EvaluationRun(含脱敏 case + grade);不含 user_id 明文
→ 外部 channel (Telegram 等)否(D-02)V1 不部署 channel
→ 公网 webhook outV1 不主动外推
→ Trading Matrix / 其他兄弟项目需另外授权契约(v1-product-object-and-schema-design.md §5.2

10. Migration Path to RDB

10.1 触发条件

满足任一即启动 RDB 迁移设计:

条件说明
用户规模 ≥ 50文件系统列表性能下降明显
多 worker 部署需要写并发
跨用户全文检索需求文件系统不适合
商业 SLA 要求事务一致性多对象事务需 ACID
Legal/Compliance 要求审计日志 immutable用 RDB + WAL

10.2 候选目标

选项评分备注
Postgres 16+推荐JSONB 原生支持;与 Pydantic v2 + asyncpg 自然
SQLite 单机扩展仅 trial 备选单机 + WAL;适合 5 ~ 50 用户中间态
文档数据库 (Mongo)不推荐引入新运维栈

10.3 迁移工序(V2 草案)

  1. 实现 PostgresCognitionStore(同 Protocol 接口);
  2. server/scripts/import_from_filesystem.py:扫描 data/,逐对象 upsert 到 PG;
  3. 双写期(≤ 1 周):FileSystem + Postgres 并行写;reads 切到 Postgres;
  4. 校验对账脚本;
  5. 切换 single-source-of-truth 为 Postgres;FileSystem 仅作 backup;
  6. 灰度迁移商业信号 SQLite → Postgres TimescaleDB 扩展(可选)。

10.4 不变项

无论是否迁移到 RDB,以下契约变:

  • 对象 ID 命名规则(§5.1);
  • schema_version / object_version 含义;
  • Refresh 语义(§6);
  • Privacy 边界(§9);
  • API 层暴露的 endpoint shape(v1-api-contract-design.md)。

11. Acceptance

本文满足 V1 工程化 B-2 任务的接收条件:

状态
14 类持久化对象都有路径 / mutability / 消费者声明(§2 + §4)
V1 选用 Filesystem + JSONL + Markdown,预留 RDB 接口(§3)
ID / 版本 / 时间字段命名规则统一(§5)
Refresh 语义保证「历史 Snapshot 不被覆盖」(§6.3)
凭证 / 私钥永不写盘;ProfileConsent 永久存证(§2.6 + §9.1)
Trace 保留 90 天后归档;CS event 撤回 48h 内删除(§7.1 + §6.5)
用户数据导出脚本与 GDPR-friendly 字段就绪(§8.2)
v1-product-object-and-schema-design.md §13 API and Persistence Boundaries 对齐
Migration to RDB 的不变项明确(§10.4)

12. Open Items

  • O-1:用户数据 backup 的 Labs 加密存储位置 / 凭证 — 待项目发起人在 trial 启动前对齐;
  • O-2:cs event SQLite 的 schema 详细字段 — 由 v1-commercial-signal-instrumentation-design.md 后续 sub-packet 细化;
  • O-3:trial 退出时的「数据脱敏」具体规则(哪些字段 hash / 哪些 drop) — 待 GDPR 法务复核(v1-engineering-kickoff-decisions.md D-12)后定;
  • O-4:是否在 V1 内引入 data/.lock 文件防止人工备份与运行时并发写 — 待 backup runbook 落地时定;
  • O-5:trace --full-trace 模式的开关配置(仅 dev 本机;不影响 trial 数据) — 待 observability sub-packet 决定。