JEV 记忆决策与迁移

可选阅读。这一页是写入决策的实现说明,写应用时不用看。

实现只保留 system1 和 dual。

写入

  1. 移除宿主注入块和 system 对话轮;保留 source 为 L0_RAW 原始证据。
  2. 生成模型只抽取明确、原子化候选文本、日期及来源,不裁决是否已知或是否更新。 原文写明有限有效期时,候选带 valid_from / valid_until(ISO 日期,按 UTC 整日展开) 与 temporal_scope;格式不合法的边界被丢弃,记忆照常写入。
  3. 读取同 scope 当前 latest 及真实 history_versions,作为比较材料。head 数超过 reconcile_candidate_limit,或材料 JSON 超过 jev_max_question_chars 时,按与任一提议的 最高 embedding 相似度排序并截断。相似度只决定拿哪些旧版本来比较,不决定是否写入: 每条提议仍进入 JEV 决策,被截掉的 head 只是不参与这次比较。路由用的 embedding 在写入时复用。 target 选项是短标签([layer/kind 日期] 正文前 240 字),完整节点只在 state.current 出现一次。
  4. 候选与当前 head / 批内较早候选的最高 cosine 低于 reconcile_relation_threshold(默认 0.6)时, 直接作为独立记忆(ADD_SUPPLEMENT 或带有效期的 ADD_TEMPORAL)追加,不调用 JEV。达到阈值时, 先问 target;选中 target 后,第二阶段再问 action,并且只提供与 target 匹配的选项。
  5. 有 target:ADD_OVERRIDE / ADD_SUPPLEMENT / ADD_NEGATE / ADD_CONFLICT / SKIP / DELETE / INVALID / NEED_CONTEXT。来源 memory_at 早于所选 head 时(乱序灌入),选项换成 ADD_HISTORICAL / ADD_CONFLICT / SKIP / INVALID / NEED_CONTEXT:旧报告不能 覆盖、否定或删除更新的 head。HISTORICAL 作为非 latest 版本插到 head 之后(新节点 superseded_by=[head],head 的 supersedes 追加它),head 保持 current。无 target:ADD_SUPPLEMENT / ADD_TEMPORAL / RAW_ONLY / INVALID / NEED_CONTEXT。TEMPORAL 不链接 baseline。SUPPLEMENT 可以与其他 latest 共存,但不携带 target(同一对象的互补片段按 ADD_SUPPLEMENT 各自独立写入,检索侧同组呈现,不做有损文本合并)。两边的说明文字也分开,无 target 时不再提到 SKIP。 SKIP 只能为一个 target 建立可引用别名;多目标共同覆盖时选 ADD_SUPPLEMENT。 DELETE 只能选择已提交的 current head。
  6. 动作和 target 不匹配是 provider 契约错误,抛 DecisionContextError,不把整批拿去重试。注入 provider 也必须精确回答全部问题, 且 answer type 与 choice 必须属于对应问题。
  7. 批量 embedding;检查 target revision、scope epoch、作用域、依赖顺序与重复消费; durable journal 后追加新节点、更新历史链接。临界区不包含正常模型 I/O;同 scope 竞争失败时重决策,最后一次重试在 scope 锁内进行。不同 scope 并行。

SKIP 是“当前事实已覆盖”,RAW_ONLY 是“没有持久增量”。原文证据不是已存知识; 历史中出现过北京,不说明目前仍住北京。北京→上海→北京必须链接当前上海版本。 旧 novelty 门和 weak_candidate_score 是两个不同概念,但本设计两个都退出写入逻辑。 对外 MemoryDecision.action 原样区分 ADD / SKIP / RAW_ONLY / DELETE / INVALID。

单次写入,不复核、不重试

每次写入只做一次抽取、一次关系决策、一次提交;没有复核队列,也没有传输重试:

情况 处理 WriteResult.status
抽取为空 不调用模型复查,来源保留为 RAW_ONLY raw_only
候选没有相似度 ≥ reconcile_relation_threshold 的 head/较早候选 直接 SUPPLEMENT(有效期时 TEMPORAL)追加,不调用 Jev committed
NEED_CONTEXT、关系置信度 < reconcile_review_confidence 当场按独立 SUPPLEMENT 写入 committed
同批多个候选取代同一 head / 指向未落库的较早候选 置信度最高者保留关系,其余独立写入;悬空关系剔除 committed
INVALID 候选 不写入,决策保留在结果里 由其余决策决定

没有产生任何版本的写入(全部 RAW_ONLY/SKIP)也在 journal 记一条空计划,重试从 journal 重放。 versions.db 的 requests 表(request_key → plan)是对应索引,已有库打开时从 plans 补齐。

仍然抛错的只有:provider 契约错误(缺题、越界选项)、动作与 target 不匹配、来源本身超出 JEV 预算(DecisionContextError,REST 422);外部写入改动了已链接的 head(StaleDecisionError,REST 409, 可原样重试);request_id 复用不同输入(IdempotencyConflictError,REST 409)。异常从 memhub 顶层导出。

dual / digest

add() 先完成同一 System1 决策和版本写入,再登记 cognition scope。 digest() 捕获队列 task IDs,同 scope 顺序处理,不同 scope 默认并行 4。

System2(v3,对齐 hy-memory 2.0.4)产出三类 L3:

  • schema:当前 L1/L2 按 average-linkage cosine 聚类(system2_cluster_distance,超大簇按 system2_refine_distance 再切,小于 system2_min_cluster_size 的簇不调用模型)。每簇一次生成, 操作为 create_schema / add_evidence,证据须跨 ≥ system2_min_sources 个独立原文来源。 DBSCAN 在单个用户的记忆上会链成一个簇,因此不用。
  • core schema:基础 schema ≥ system2_cross_domain_min_basics 时,按去领域的行为抽象成组, 每组至多一条跨领域 schema,证据继承成员。
  • persona catalog:按 8 个领域从当前事实中选择原文(不改写),事实集合变化时才重建。

读取默认 derived_context_limit=0(schema 只写不读)、catalog_context_enabled=True。 生成失败只丢弃该产物,不重试;未提交的事实保持未处理。 来源更新后,引用该来源的旧推导变成非 latest,其余证据恢复 fresh;下次 digest 重建。 图投影待处理记录与版本同一提交;投影失败时,下次 digest 先恢复,已处理证据不会导致漏补图。 新到达的 task IDs 不会被旧水位确认。失败 scope 保留 pending,成功 scope 可独立确认。 旧 reconcile 队列和迁移 worker 已删除;新目录仅登记显式 digest 任务。

历史、恢复与公共 API

  • update(id, content) 返回新 ID;旧 ID 仍能 get 原文,且 is_latest=false。
  • delete(id) 归档当前表示,保留记录。delete_bulk(confirm=True) 为用户明确清除 scope, 是独立的物理删除 API,不属于 reconcile。
  • list(..., include_history=True) 查看历史;默认 list/search 只读取 latest。
  • WriteResult.decisions 直接返回 action、update_type、targets、proposal_ids、source_ids、confidence。
  • MemoryItem 暴露 layer、kind、revision、有效期、source_ids、supersedes、conflicts_with。
  • add(..., request_id=...) 允许相同输入幂等重试;另一个事件必须使用另一 request_id。 重试恢复 durable plan,重复文本不依赖 content hash 去重。
  • versions.db 保存完整计划(plans)、审计事件、跨设备复制日志(log,按 (origin, seq) 递增并带向量)与删除墓碑(tombstones);versions.lock 协调本机进程。进入存储读操作先校验现存 schema 与全部待回放计划的 schema,再恢复未完成计划;旧格式拒绝且不改写。非法向量在写日志前拒绝。多台设备之间通过导出/导入 log 条目(export_log / import_log)异步复制并按字段收敛合并,不在多台机器间直接挂载同一个 Chroma 目录做并发写。
  • persist_history 控制附加审计库;版本内容和恢复日志始终保留。
  • 幂等结果存在 cache.db 的 request_outcomes 表(不再借用 profile 缓存),digest 时清理 超过 request_outcome_ttl_seconds 的记录。

读取

  • search(as_of=...):显式有效期不包含该时刻的记忆被过滤(如已过期的 TEMPORAL)。 benchmark 传问题时间;不传则不过滤。
  • search(include_relations=True)(默认):命中带一跳 previous(直接被取代的旧值, HISTORICAL 也会出现在这里)与仍然有效的 conflicts;formatter 渲染为 (previously, 日期: …) / (conflicts, both unretracted: …),有效期渲染为 (valid: 起 → 止)。
  • search(raw_limit=N):L0 原文兜底路,语义 + BM25 做 RRF,分数压到 ≤0.5,结果在 SearchMemories.raw,flatten() 会合并。默认 0 关闭。

配置与限制

JEV 可使用 jev= 注入 provider,或配置 jev_api_key / jev_endpoint / jev_model。 Settings.from_dict({"jev_api_key": ..., "jev_endpoint": ...}) 只接受规范平铺配置。密钥放 jev_api_key 或 MEMHUB_JEV_API_KEY。 默认并发 4,HTTP 客户端复用,429/529/502/503/504 有限重试;按问题数量和字符预算拆包。 字符预算是保守配置,不是 tokenizer 实测 token 数。

材料上限默认 64、最多 220;每个 target choice 至多 255 个选项。选项文本是完整节点 JSON(layer、kind、revision、有效期、source_ids),不只是正文,所以更容易先撞上 jev_max_question_chars。提议特别多时应拆 source。 默认提议问题批量 12。材料/历史过长或动作与目标矛盾时显式报错并保留 source, 不会无依据地转成 SUPPLEMENT。

删除配置:reconcile_sync、reconcile_skip_llm、reconcile_weak_candidate_score、 reconcile_policy、reconcile_non_destructive、content_hash_dedup 以及 cluster 写入门控。 旧键及嵌套 mem0 配置不再兼容,Settings 校验直接报错。默认 storage_dir 是 ./.memhub_data。显式打开 layer_schema_version 不是 4、或状态不是 ACTIVE 的目录时抛 ValueError 并且不改写文件。

Coding 场景:procedure 记忆

chat 和 coding 不是两套系统。场景是每条数据的属性:带工具轨迹、被判为工程会话的写入, 除了 chat 抽取,还另外产出 kind="procedure" 的 L1 记忆。存储、scope、版本链、 演化链接、apply_version_plan、幂等和 System 2 队列全部复用主路径。

  1. 先留来源:L0_RAW 只存用户与 assistant 的对话正文,之后任何模型调用失败都不丢来源。 工具输出(文件内容、命令日志)是过程材料,不进 L0、不进任何模型调用;工具调用只用来 结构化提取 files(path / file_path 等参数)。
  2. scene:messages 里有 role="tool" 或 assistant tool_calls(结构条件)时,一次 JEV choice 判 CODING / CHAT。CHAT 去掉工具消息后只走主路径;CODING 两路都走。
  3. 两路抽取并行:chat 抽取输入与没有工具轨迹时相同(工具消息剔除),prompt 不变; procedure 抽取一次 LLM 调用,输出 {"candidates": [...]},字段 task、search_keys、 solution、boundary_envs、boundary_scope、confidence;files 由 tool_calls 的路径参数 结构化提取。空候选不调用 JEV。
  4. 互不可见:chat 裁决的候选池排除 procedure;procedure 裁决只看同 scope 内 kind="procedure" 的 current head,且只看同一版本边界(workspace / branch / boundary_scope,strict 还要求同一 session)。有可选 head 时先选 target(含 NONE / MULTIPLE),再按是否选中给出 action;head 超过 candidate_limit 时先分页 INCLUDE / EXCLUDE。
  5. 执行:裁决结果转成主路径的版本计划。OVERRIDE / NEGATE 追加新版本并退役 target;DELETE 只归档;SUPPLEMENT / TEMPORAL / CONFLICT 不退役任何 head(工程经验不做 MERGE,补充新信息时两个版本都保持现行,避免合并重写丢掉旧版本里的约束)。 同一批多个候选替换同一个 head 时,head 退役一次并链接全部后继。INVALID 只丢弃那一条候选, NEED_CONTEXT 写成独立版本,都不让整次写入失败。
  6. 检索:hybrid 的 chat 各路(profile / proactive / normal / 派生融合)过滤掉 procedure; procedure_limit > 0 时另跑一路只召回 procedure,有自己的配额和阈值 (procedure_min_score,默认 0.3),结果放在 SearchMemories.procedures。两组分数从不互相比较。
  7. System 2:procedure 写入同样登记待处理 scope;归纳输入包含 procedure, 人格目录(catalog)只引用对话事实。

对外结果:WriteResult.scene 为 chat / coding,procedure_ids 是新 procedure 版本 id (同时在 extra_node_ids 里);decisions 合并两路裁决。MemoryItem.procedure 给出结构化字段。 update / delete / get / list 与其它 L1 记忆一致。