# Memory Curator Method Zh

> memory_curator method — leaf events 用 prose body + 三段可选 section (不可逆 / 未结 / 原文摘录) 格式 (见 event-summary-rules-zh); rollup compress 用 depth-aware outline 格式 + 跨 children 主题归并; thread title 必须编码 resolution 条件; 写作纪律 (§2 颗粒度 / §2.2 心理 A/B/C / §4 十类黑名单) leaf 与 rollup 共享。

- Skill: `funnycups/memory-curator-method-zh` (Agent Skill)
- Install (CLI): `npx skillmds@latest add funnycups/memory-curator-method-zh`
- Raw SKILL.md: https://api.skillmd.com/api/skills/funnycups/memory-curator-method-zh/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: funnycups (https://skillmd.com/u/funnycups)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/funnycups/memory-curator-method-zh

---


# memory-curator-method-zh

## Agent 多轮工作流硬要求

- 每次写 event.summary 的工具调用 (`memory_node_create({type:'event'})` / `memory_compact_nodes` / `memory_node_edit` 改 event.summary) **之前**, 在响应里完整跑一遍 `event-summary-rules-zh` 的 **§5 写作流程 (Step 1a / 1b / 2 初稿 / 3 单句判定 / 3 终稿 / 4 sections 判定 / 5 黑名单扫描 / 6 最终 prose) + §6 最终自检**。Step 6 产出 → `summary` 字段值 → 工具调用。所有 Step 都必须把具体内容写入 <thought> 块 (不允许 "see below" / "drafted in summary" / "略" 这种省略)。
- **多组连续压缩**: groups 列表里每一组各自一次完整 CoT + 各自一次工具调用。**严禁批量、严禁跨组复用 thought、严禁在第 N 组开始只写 Step 6**。压缩第 5 组时跟压缩第 1 组应该是同样的认真程度。
- routine 兜底事件 (路过 / 休整 / 单次场景) 也要完整跑 §5 流程 — 别图省事直接写 Step 6 产物。短摘要不等于跳步; routine 时 prose body 可以是单句话, 但 §5 整个流程不允许跳。
- 工具调用的 `summary` 字段必须与 <thought> 块的 `最终 prose:` 段**byte-equal** (空格 / 换行 / 标点完全一致)。

---

## 核心原则 (type-specific)

**event 类型: 每次 dispatch 必出一个 event 节点**。即便本轮只是路过 / 休整 / 闲聊 / 单次场景 — 仍 emit event。**最终 summary 字符串**可以只一句话 (routine 时: 交代时间、地点、人物动作即可), **但 §5 写作流程必跑** (见上方 Agent 多轮工作流硬要求)。压缩端在 rollup 时按 `event-summary-rules-zh` 的 §2 颗粒度 + §2.3 单句删除测试过滤 routine noise; 但**叶层必须有连续 event 流, 否则时间线断裂、recall 无法重建剧情上下文**。

高后果事件 (契约 / 誓言 / 婚约 / 师徒关系建立或破裂 / 不可逆物理状态变化 / 长期身份 / 立场变更 / 新角色登场 / 地点 controller 变更 / 重要物品转让) 的 summary 字符串写完整 prose body + 必要时填入 `不可逆` / `未结` / `原文摘录` section (三段默认空, 仅在 `event-summary-rules-zh` §3 三条全 Y 时才出现)。

**其他类型 (character_sheet / location_state / thread / 自定义) 默认 SKIP**。写错的代价高 (LLM 编属性)。emit 的门槛因 type 而异:
- character_sheet / location_state: 候选变更在故事时间往后 24h 之后仍约束故事走向。"不一定" / "取决于场景" → 不写。
- thread: 必须通过"跨场景门槛" — 这个钩子真的需要后续 ≥1 个独立场景才能解决 / 触发 / 结束。同场景内位移、单场景就解决的小目标、短期承诺 → 都**不**是 thread, 走 prose body 内部叙述就够了。详见下方 thread 节点专章。

不要把单次场景姿态、当前心情、临时性服务关系、对话氛围、未付诸行动的情绪、引述的对白原文写进 character_sheet / location_state / thread 等字段。

**先查再写**: 对每个本轮出现的实体 (角色 / 地点), 写之前必先调 `memory_find_by_name`; 命中后调 `memory_node_brief` 看现状, 再决定 create / edit。**严禁不查就 create** — 同名节点重复 create = 图污染, 比错写更难修。

## 多轮迭代

你是多轮 agent, 可以反复调用工具。每次拿到工具结果后再决定下一次调什么 — 不要一次响应里把所有判断和写入都堆完。

**终止条件**: 本轮该写的都写完后, 发一条**不含任何工具调用**的响应 (响应文本写一句话总结你做了什么, 例如: "写 1 event + 1 character_sheet edit")。runtime 检测到无工具调用即终止 sub-agent loop, 这段文本作为本次 dispatch 的返回值给主 agent。这条总结是给主 agent 看的结构化简报, 不要写成角色扮演正文、对白或旁白。

## 工作流 (必须按顺序)

### Phase A — 抽取

1. **查 schema**: 调 `memory_schema` 一次, 确认当前 schema 的字段、editable 类型、关系词表。这是便宜的一次, 后面所有判断的依据。

2. **拉近期 event 看上下文**: 调 `memory_list_candidates({ types: ['event'] })` 看最近 rollup event 列表 (recency-first 排序; hierarchical 压缩已开, 你看到的是高层 rollup, 不是 leaves)。对最近 1~2 个 id 调 `memory_node_brief` 看 summary。**这告诉你时间线写到哪了** — 本轮 event 从哪儿接着写 (避免重复上一条, 也避免漏掉中间没写的对话)。如果某 rollup 明显是本轮要延续的场景且 childCount 大, 可调 `memory_expand_seeds({ seed_ids: [<rollup_id>], hops: 1 })` 下钻看具体 leaves。

3. **查现有节点**: 对本轮出现的每个角色 / 地点的中文名 / 别名, 调 `memory_find_by_name({ query: <name> })`。返回的 matches 列表告诉你"该实体已存在 / 不存在"。这是 create vs edit 的决策依据。

4. **拉详情**: 对每个 `find_by_name` 命中的 id, 调 `memory_node_brief(id)` 看它当前的 fields、aliases、edges。**只有看清现状, 才能决定字段是否需要 patch、关系是否需要 upsert/delete**。

4b. **dedup roll-call (硬约束, 在响应文本里显式产出, 跑完 find_by_name + node_brief 之后立刻写)**:

   在做任何 `character_sheet` / `location_state` / `thread` 的 create / edit / SKIP 决定之前, 必须先按下面格式对**本轮出现的每个**角色 / 地点 / cross-scene 钩子逐项点名汇报检索结果。这一段是**强制结构**, 不是建议:

   ```
   [dedup roll-call]
   角色:
     - 名: 张三
       find_by_name 检索: title 精确匹配 = sheet_zhangsan; aliases 重叠 = (无)
       决定: SKIP (已存在, 本轮无 24h+ 字段变化) / EDIT sheet_zhangsan (变更字段: identity, 原因: 关系状态从 X 转为 Y) / CREATE (无匹配, 原因: 本轮首次出场)
     - 名: 李四
       find_by_name 检索: title 精确匹配 = (无); aliases 重叠 = sheet_someone (因为 sheet_someone.aliases 含 "李四")
       决定: SKIP / EDIT sheet_someone / CREATE — 选 EDIT (复用别名命中的节点) 或 SKIP 不能 CREATE
   地点:
     - 名: 某地
       find_by_name 检索: title 精确匹配 = (无); aliases 重叠 = (无)
       决定: CREATE (确认无匹配) / SKIP (本轮未引入长期 controller / danger / resources 变化)
   thread (本轮新钩子 + active threads 全量扫描):
     - 名: 某 cross-scene 钩子
       决定: CREATE (触发条件: ... 匹配类别 1/2/3 中的哪个 + 通过跨场景门槛) / SKIP (未通过跨场景门槛, 写入 prose body 即可)
     active threads 扫描 (对 graph_data 里每个 status=active 的 thread 逐项判断):
       - thread_id n_X "某线索 A": SKIP (本轮未触发) / EDIT note (推进了, 新进度: ...) / EDIT status=resolved (本轮 event prose / 不可逆 section 含 ... 已达成) / EDIT status=abandoned (本轮 ... 让其失效)
       - thread_id n_Y "某线索 B": ...
     如果 graph_data 无 active thread, 写 "active threads: (无)"。
   ```

   硬约束:
   - **任何 `memory_node_create(type=character_sheet|location_state|thread)` 工具调用前**, 该实体必须在 dedup roll-call 中出现, 且决定字段 = CREATE。直接 create 一个未出现在 roll-call 里的实体, 该次响应作废。
   - roll-call 必须**逐个**列出本轮每个出场角色 / 地点以及每个 active thread (即使最后决定 SKIP)。不允许"全部都是新的"或"无变化"这种简写。
   - title 精确匹配 + aliases 重叠两项**都必须**报告 (不能省略其一)。如果 find_by_name 无任何匹配, 也要显式写"matches 为空, 检索结果均为 (无)"。
   - 决定 EDIT 时必须**指明变更字段 + 原因** (具体哪个字段从 X 改成 Y, 不能写"补充信息"这种含糊话)。
   - 决定 SKIP 时必须说明**为什么本轮该实体的长期字段 (traits / identity / goal / controller / state 等) 没有 24h+ 变化**, thread 的 SKIP 必须说明"本轮 event prose 是否触及该 thread 的目标 / 进度 / 解决 / 放弃"。

   **Thread sweep 硬约束**: dedup roll-call 的 thread 段必须完整扫描 graph_data 所有 `status=active` 的 thread。**只要 event prose / 不可逆 section 提到的内容能回答"是, 该 thread 的 title 描述的目标已达成"**, 必须 EDIT status=resolved。stale thread (event 已经解决 / 推进了它但 thread.note 没更新) = 严重错误。

   为什么这条强制: 模型容易"查了 find_by_name 但没认真看 brief" — 工具结果如果只是被动消费, 模型注意力分散时会漏匹配, 导致重复 create 出同名节点污染图。强制 roll-call 把"查"变成显式产出步骤, 跟下游工具调用必经路径化。Thread sweep 同理: active threads 容易被遗忘, 强制每轮逐项扫描+决定, stale 不再有借口。

5. **判定**: 对每条候选变更, 自检"这个事实在故事时间往后 24 小时之后还约束故事吗?"如果答案是"不一定"或"取决于场景", **不写**。

6. **写**: 调 `memory_node_create` / `memory_node_edit` / `memory_node_delete` / `memory_link_upsert` / `memory_link_delete` 落地。
   - **event 节点 summary 字段的写入 (create / edit)**: 工具调用前必须完整跑一遍 `event-summary-rules-zh` 的 §5 写作流程 (Step 1a → 6) + §6 最终自检, 所有 Step 都把具体内容写入 <thought> 块。工具调用的 summary 字段必须与 Step 6 `最终 prose:` byte-equal。
   - **character_sheet / location_state / 自定义类型 / event 的非 summary 字段 (aliases / traits / 等) 的写入**: 每次工具调用前一句简短中文说明意图即可, **不需要 §5 写作流程**。把它们当作正常的多类型抽取出 — 不要被 event 的写作流程要求误推。

7. **event 兜底 (per-call 结构化输出, event summary 字段必须走 §5 写作流程)**: 完成 stable-fact 写入后, 如果还没写 event, **必须调一次** `memory_node_create({ type: 'event', fields: { summary: <按 event-summary-rules-zh 要求产出的字符串> } })`。
   - **调用前**先在响应里完整跑一遍 `event-summary-rules-zh` 的 §5 写作流程 + §6 最终自检, 把 Step 6 最终 prose 作为 `summary` 字段值。
   - 即便本轮 routine 也要 emit。漏了 event 会让时间线断裂 — 这是硬要求。

### Phase B — 压缩

抽取完成后, 对每个声明了 hierarchical 压缩的类型 (从 schema 看 `compression_mode`):

1. **查可压缩组**: 调 `memory_compaction_candidates({ type, depth: 0 })`。返回的 groups 是当前可压缩的孩子节点组, 按 fanIn 切好。空数组 = 当前不需要压缩, 跳过该 type。

2. **拉每个 child 的 brief**: 对 groups[i].childIds, 逐个 `memory_node_brief` 看 summary 字段 (含 prose body + 任何 section)。

**压缩专属铁律 (compression-only, 在 event-summary-rules-zh 写作纪律之上叠加, 应用于 Phase B 每一组压缩)**:

> **Leaf vs Rollup 输出格式差异 (核心)**: leaf event 的输出格式是 `event-summary-rules-zh` §1 的 **prose body + 可选 section**; rollup 的输出格式是更紧凑的 **outline (数字编号条目 + 时间区间头部)**。原因: rollup 进入持久注入路径, 需要密度高、易扫读, 不需要 prose 的叙事连贯性。**但**: prose body 的颗粒度准则 (§2)、心理 A/B/C 分类 (§2.2)、单句删除测试 (§2.3)、十类黑名单 (§4) **完全适用**于 rollup 的每一条 outline 条目 — 形式不同不代表纪律可以放松。

> **Cross-children 主题归并 (核心)**: 压缩 ≠ dedup。把 N 个 children 的内容拼起来再去重不是压缩, 是合并。真正的压缩做**主题归并**: 在 children 之上抽出同一主体的同类动作 / 同一关系节点的多个侧面 / 同一活动范畴的连续事件, 用单条父类条目覆盖。
>
> **归并触发信号 (任一为真 → 必须归并, 不得保留为多条)**:
> - 多个 children 共享相同主体 + 同类动作 + 不同对象 (例: child1 「X 与 A 做 Z」 + child2 「X 与 B 做 Z」 → 单条 「X 先后与 A、B 做 Z」)
> - 多个 children 围绕同一关系节点 (X 与 Y 关系演进) 的不同侧面 → 合为该关系节点的单条
> - 同一活动范畴 (社交 / 战斗 / 性事 / 移动 / 协商 等) 在连续时段内多次发生 → 合为该活动范畴的单条
> - 多个 children 描绘同一受赠 / 告白 / 决裂 / 相遇等关系标志事件的不同细节侧面 → 合为该关系标志的单条

> **Depth awareness (核心)**: 每次压缩跑前都要先确定 `rollup_depth` (输出 rollup 的层级 — children 是 depth-1 层)。**对于 memory-graph 自动压缩, 系统会在 compress prompt 后段以 "Compression context (HARD, from code): rollup_depth=N" 显式注入这个值, 直接读取**。**对于 director memory_curator 走 `memory_compact_nodes` 工具的手动压缩, 看 children 自身的 depth (`memory_node_brief` 返回值里有 `semantic_depth`) — children 是 leaf (semantic_depth=0) 则你产生的 rollup 是 depth=1, children 是 depth=1 rollup 则你产生 depth=2, 以此类推**。**rollup_depth 越大, 输出越少条 outline 项, 信息密度越高**:
>
> | rollup_depth | 资讯密度 | 适用 |
> |---|---|---|
> | 1 (children 是 leaf events) | 章节级关键事件 — 仍可保留 关键决策 / 不可逆变化 / 未结线索 的紧凑 outline 条目 |
> | 2 (children 是 depth-1 rollups) | 章节级里程碑 — 只留长期影响后文的事件, 砍单场景甚至单天细节 |
> | 3+ (children 是 depth-2+ rollups) | **章节标题级** — 只保留章节级转折、跨整章节都成立的关系定性、章节结束时的最终状态。所有日常 / 单次性事 / 临时遭遇 / 战斗细节 / 非关键决策一律砍光 |

> **信息密度对称, 不与 children 字数对称**: rollup 长度应当**显著短于** children 总和 — 只保留跨场景结构 + 不可逆里程碑, 砍掉任何单场景的细节素材。不用追求某个具体的压缩比, 由"上面优先保留 / 优先砍"的判据决定每条 outline 的去留。如果 rollup 与 children 总长相近, 几乎可以确定退化成了 concatenation, 重审。

> **覆盖完整性 (硬约束)**: rollup 的时间区间必须覆盖所有 child events 的时间联合 (取所有 children seq_to 范围的最早值 → 最晚值)。每个 child 的核心不可逆事件 (失贞 / 怀孕 / 告白 / 决裂 / 获赠关键物品 / 死亡 等) 必须在 rollup 中以某种方式承载 (可以独立成行, 也可以归并入跨 children 的父类条目, 但不允许直接丢弃)。如果你的 rollup 时间窗短于 children 时间联合, 或某个 child 的核心事件完全没出现在 rollup 中, 这是失败信号, 必须重写。

> **rebuild 场景的输入重叠去重 (硬约束)**: 在 rebuild 重建场景下, 相邻 children 可能存在事件重叠 (因为每个 batch 包含 prior context, 早期版本的提取器会在多个 batch 各自的 summary 里都覆盖同一个底层事件; 同一条不可逆变化 / 同一个 thread 引用 / 同一段 verbatim quote 也可能在多个 children 各自的 section 里重复)。判定重叠: 两个 children **指代同一底层事件** (相同主体 + 相同类别动词 + 相同对象 + 时间锚重叠或相邻) → 视为同一事件, 在 rollup 中只出现一次, 时间锚取**该事件的起始时间**。同样地, 同一条不可逆里程碑 / 同一个 thread 名 / 同一段 quote 在多 children 中重复出现 → 在 rollup 里只承载一次。

> **反模式 (出现即重写)**:
> - 给每个 child 的所有 prose / section 内容全数搬运再删几条重复 (= dedup, 非压缩)
> - 给每个 child 平均分配 outline 行数 (rollup 应按"主题 / 关系节点"分配, 不按 child 平均分配)
> - 增加 children 里没有的位移 / 铺垫类条目 (「X 返回 Y」 「X 准备 Z」) — 位移和铺垫属于连接性纹理, 在 rollup 层应被父类动作吸收, 不单列
> - 跳过某段时间窗的事件 (例: 4 个 children 覆盖 08:00-10:00, 你的 rollup 只写 09:30-10:00) — 这是信息丢失而非压缩
> - **临时角色任命扫描**: rollup 里不应出现"大堂经理 / 临时管家 / 客串店员"这种临时身份的条目 (这些在 depth=1 都该砍, depth=2+ 一律砍)
> - **在更高 depth 里保留单次性事 / 单场景调情 / 临时角色任命** — 这些 depth=1 该砍, depth=2+ 一律砍

> **A 类具体动词的 depth-conditional 抽象 (核心)**: A 类 = 具体性事动词 (肛交 / 口交 / 深喉 / 骑乘 / 后入 / 指交 等) + 具体战斗招式 + 具体动作动词。
> - leaf event 写作时 (extract / Phase A 第 6 步 event 写入): **允许**在 prose body 中以**体位概括**形式出现 A 类动词 (一次, 一个名词指代姿势, 不展开过程), 详见 `event-summary-rules-zh` §2.1 性事 / 战斗段。
> - rollup_depth=1 时: 多次同类 A 类动作**必须合并**为通用动词 ("X 与 A、B、C 先后 性事" / "队伍 击退 X 类敌军"); 单次独立关键事件可保留具体动词。
> - rollup_depth ≥ 2 时: A 类动词**不允许**出现, 全部抽象为通用动词 ("X 与 多名女性 性事 (附失贞名单)" / "队伍 攻陷 X 据点")。
> - **B/C/D 类禁令仍 depth-invariant**: 现场命名 (正式 X / 终身 X / 宣告归属) / 元叙述 (钩子 / 伏笔 / 为后续) / 契约词族 / paraphrase 残留 — 所有 depth 都禁, leaf 也禁。

> **压缩端的 thought 块强制可见**: 跑 `event-summary-rules-zh` §5 写作流程时, 把 Step 1a (这里 = "枚举 children 的事件锚 / 不可逆条目 / thread 引用 / 摘录") 到 Step 6 (这里 = "最终 outline + 时间区间头部") 全部写进 <thought> 块, 不允许 "see below" / "drafted" / "略"。这与 leaf 同步, 是审计 cross-children 主题归并是否真的做了的唯一手段。


3. **逐组压缩 (每组独立一次 memory_compact_nodes 调用, 严禁批量, 每次调用前都跑完整 CoT)**: groups 列表里每一组按下面顺序处理:
   a. 取本组 `childIds` 对应的 child summaries (step 2 已拉) 作为本次「事件来源」。
   b. 在响应里完整跑 `event-summary-rules-zh` 的 §5 写作流程 (Step 1a / 1b 在这里改写为枚举与归并 children 的 prose / sections; Step 2 输出 outline 初稿; Step 3 单条删除测试; Step 4 不适用 sections — rollup 用 outline 不写三段 section; Step 5 黑名单扫描; Step 6 时间区间头部 + 最终 outline) + §6 最终自检, 逐项打勾。**不可跳步、不可压缩到几行带过、不可复用上一组的 thought 改两个词**。
   c. 把 Step 6 最终 outline 作为 `summary` 字段值, 调一次 `memory_compact_nodes({ type, child_ids: groups[i].childIds, summary: <Step 6 最终 outline> })`。
   d. 进入下一组, 从 a 重新开始。每组各自一次完整 CoT + 各自一次工具调用。

4. **同 depth 内 cascading**: depth=0 全部压完之后, 再调一次 `memory_compaction_candidates({ type, depth: 0 })` 看是否还有新可压缩组。空了再往上 depth+1 重试。最大 depth 取自 schema 的 `compression.maxDepth`, 通常 ≤ 10。

### Phase C — 收尾

本轮所有写入完成后, 发一条**不含任何工具调用**的响应, 文本写一句话总结你做了什么 (例如: "写 1 event + 1 character_sheet edit; 压缩 2 组")。runtime 检测到无工具调用即终止 sub-agent, 这段文本作为 dispatch 返回值给主 agent。**不要**调任何虚构的 "结束工具" — 没有那个工具, 叫了也会报 tool execution unavailable。这条总结是给主 agent 看的结构化简报, 不要写成角色扮演正文、对白或旁白。

## 字段与边规范

- **字段范围硬规则**: `memory_node_create` / `memory_node_edit` 的 `fields` 对象, key 必须 ⊆ 该 type schema 的 `tableColumns`。**写入前如不确定就再调一次 `memory_schema` 确认**。写到 tableColumns 之外的 key 会被 op pipeline 静默吞掉 (不报错), 节点只保留你以为没写的旧值 — 这是最容易踩的坑。
- **required columns 必填**: schema 中标 `requiredColumns` 的列 (典型: `character_sheet` 的 `title`, `event` 的 `summary`) 在 `memory_node_create` 调用里必须有非空值。`memory_node_edit` 不允许把 required 列清空 (`clear_fields` 不许包含 required)。`memory_schema` 返回的 type spec 里有 `requiredColumns` 列表 — 写入前对照检查。
- **零引号规则**: event.summary 的 prose body 内不出现任何 `"..."` / `「...」` / 中英文引号包裹的内容 — 那些走 `原文摘录` section, 详见 `event-summary-rules-zh` §3.3。真专名去引号写出; 原对白引述属违规, 改写成动作描述或路由到 `原文摘录` section。
- **禁元描述总结尾**: 事件停在动作结束。禁止附加作者口吻给事件画圈的结句, 如"确立 XX 锚定节点" / "升格为 XX 态" / "标志 XX 转变" / "形成 XX 闭环" / "核心 X 终极 Y 节点"。下游 LLM 从上下文自行得出意义, 节点不预设结论。
- **禁自创状态机标签**: 禁止把角色心理量化成 "XX 态" / "XX 波段" / "XX 消费" 之类自造可枚举术语。写可观察行为, 让下游 LLM 自行解释状态。
- **禁续写 / 伏笔 / 未来时**: 任何字段 (尤其 summary) 内不出现 "为 X 埋下伏笔" / "暗示后续" / "为后续...预留" / "钩子" 等。只写已发生, 不预测后续 — 后续剧情由下游 LLM 接生成, memory 节点不做未来时预言。如果某事件真的留下了 cross-scene 钩子, 走 thread 节点 + event.summary 的 `未结` section, 不在 prose body 里写 narrator 评论。
- **专名格式**: character / location 的 `title` 是核心名, 不带势力 / 职位 / 种族前缀, 不含括号 / 双语对照。别名进 `aliases` 列。
- **Alias 主动收集**: 对话出现昵称 / 短名 / 称号 / 英文名 / 翻译名 / 拼音时, 即便 title 已是规范名, 也要主动 patch 到 `aliases` 列 (`memory_node_edit({ node_id, set_fields: { aliases: [...] } })`, 合并去重)。aliases 是 recall 命中的主路径, 漏收一个就少一条命中通道 — 不要 SKIP (前提是该 alias 通过了 `event-summary-rules-zh` 同步的写入门槛, 见 `default-prompts.js` DEFAULT_EXTRACT_SYSTEM_PROMPT 的 alias 写入硬门槛章节)。
- **关系词表**: 只能用 canonical vocabulary —
  - 通用: related, involved_in, occurred_at, mentions, evidence, updates, advances
  - 角色对角色: partner_of, family_of, allied_with, hostile_to (对称); mentor_of, sworn_to, debt_owed_to, deceiving (单向, from 是动作发起方)
  - **规范化语义 (强制)**: "实体 / 角色参与或物质性涉入事件" 一律用 `involved_in`; "事件发生在某地点" 一律用 `occurred_at`; 弱联系且无更锐利 canonical type 匹配时用 `related`。
  - **禁词表漂移**: 不要把同义中英文 / 近义词当独立 type 用。禁用例: 参与者 / 涉及主角 / participant / main_character (应用 involved_in); 发生地 / 发生在 / 发生于 / occurred_at / happened_at / location / located_at / happened_in / occurs_at (应只用 occurred_at)。
  - **禁内部边**: 不要通过 `memory_link_upsert` 创建 `contains` / `semantic_contains` — 层级边由图系统自己管, 不属于语义抽取范围。
- **关系破裂用 delete, 不用 replace**: 复合关系 (`A→partner_of→B` + `A→deceiving→B` 同时成立) 是合法状态, 不要为了"换"而 delete。只有关系真正不再成立 (分手 / 联盟瓦解 / 债务清偿 / 誓约撤销) 才删边。
- **Link locator 硬规则**: `memory_link_upsert` 的 target 必须用真实 `node_id` (从 `memory_find_by_name` 命中或本轮 `memory_node_create` 返回值拿到), 禁止用 title 或 type 字符串模糊匹配 — 多个同名节点存在时会粘错。流程: target 已存在 (find 命中) → 用其 id; target 未命中且本轮有该实体参与 → 先 `memory_node_create` 拿到 id 再 link, 不要假设"同名就是同一节点"。漏 link 比错 link 更危险: 对每条有证据的关系都要落地, 即使要先 create 缺失的 target。
- **language_sample**: 是该角色在不同场景下的稳定说话风格样本, 按场景维度 ≤ 3 个 (例: 工作场景 / 与亲近者私下 / 战斗紧张时)。已记录的样本只在角色经历**身份 / 立场层面的根本转变**时整体重写 (faction switch / brainwashing / awakening / 长期角色转型); 新场景出现且与已记录场景实质不同时可追加 (总数 ≤ 3); **单次场景内的语气波动不算变更, SKIP**。详见 `default-prompts.js` DEFAULT_EXTRACT_SYSTEM_PROMPT character_sheet 段的稳定性硬锁。
- **事件 summary 时间 / 地点头部 (硬规则)**: 必须以 `时间: <具体时间>` 和 `地点: <具体地点>` 两行开头, 之后空一行再开始 prose body。时间用 in-world 完整年月日时分格式 (现代 RP 用 `YYYY-MM-DD HH:MM`, 历史 RP 用纪年加时辰, SF 用对应历法); 禁用 placeholder (`x年x月x日` / `未知时间` / `待定时间`)。详见 `event-summary-rules-zh` §1 输出格式。
- **identity 字段**: 只写长期身份 / 背景。临时身份 (服侍员 / 临时随从 / 患者) = SKIP。
- **location_state.state 路由测试 (硬要求)**: 写入前问 "故事时间往后推 24 小时, 再有别人到这个地方, 他 / 她还会观察到这条吗?" ✅ 会 → 写进 state / resources; ❌ 不会 → 路由到 event.summary 或 DROP。state 是地点 "长期身份证", 不是事件流水。
- **location_state.state 应写**: 长期归属 / 用途定性 ("X 的据点" / "X 的私密空间"); 跨多次访问稳定的关系性事实 (门槛: ≥3 次同类事件 OR 持续 ≥1 周的关系据点); 不可逆的物理 / 控制权变化 ("已被占领" / "已封印" / "已解锁"); 长期标志性事件锚点 (极少数: 地点因某事件被永久定义)。
- **location_state.state 不该写** (全部走 event.summary 或 DROP): 单次访问事件流水 (时间戳 + 动作 + 对话); 活动留下的临时物理痕迹 (体液 / 衣物散落 / 按印 / 湿润感 / 灰尘脚印等); 临时角色状态 (睡相 / 单次穿着 / 单次表情 / 姿势 / 心情); 单次对白引述 / 视线 / 表情 / 肢体反应; 瞬时感官 (空气味 / 温度 / 光线 / 声响); 已发生事件的镜头编号 / 具体姿势 / 动作次数清单; 事件流水信号词 ("本批次" / "本次" / "刚刚" / "目前已" / "现已" / "已完成"); 拟人化事件升华 ("见证 X" / "承载 XY" / "X 的舞台"); 关系条款细节 (金额 / 协议名 / 约定内容 — 属相关角色 character_sheet)。
- **location_state.state 长度上限**: ≤ 50 字 (中文) / ≤ 30 words (英文)。超过几乎必有事件流水混入, 回头检查每个短句能否挪去 event.summary。
- **location_state.resources**: 长期常驻设施 / 家具 / 视觉特征 / 地理特征。不带事件痕迹 ("某契约存放于此" 应进 event 不进 resources); 单次出现的临时物品 = DROP。
- **location_state.controller**: 当前实际控制者。可接受 "X (名义) / Y (实际)" 双层。不写 "X 临时担任 Y" — 除非"临时"已成长期状态。
- **location_state.danger**: 风险等级 + 主要威胁来源。不写单次访问遭遇的具体冲突 (那是 event)。
- **location_state.aliases**: 真正的别称 / 简称 / 双语名 / in-world 通称。不重复 name; 不把其他子节点名当 aliases 塞进来 (套房 aliases 不应写所属会所名)。

## thread 节点专章 (高门槛, 但不为零)

thread = 剧情线 / 伏笔 / 长线任务 / 跨场景承诺。**门槛高**, 但**不为零**。默认 SKIP, 但当本轮出现下述类别之一的钩子时, MUST CREATE。

### 跨场景门槛 (硬约束, 在判断任何类别前先过这道关)

CREATE thread 之前, 必须显式确认: 这个钩子**真的需要后续 ≥1 个独立场景才能解决 / 触发 / 结束**吗?

**"独立场景"** 的判定:
- 时间线推进 ≥30 in-world 分钟 + 主要参与角色至少一人改变 (有人来 / 有人走 / 有人换队)。
- 或地点变更到完全不同的另一场景 (不是从同一空间站的舱段 A 到舱段 B, 而是不同星球 / 不同船 / 不同据点)。
- 或对话 / 事件焦点从一个目标完全切换到另一个无关目标。

**典型反例** (不要 CREATE thread, 写进 event prose body 描述即可):
- 队伍从舱段 A 走到舱段 B 与某 NPC 会合: 同场景内位移, 下个 event 就完成。
- 角色提议 "危机解决后一起吃饭": 短期承诺, 下一两个 batch 就触发。
- 队伍计划 "先打 X 然后撤退到 Y": 单场景内的战术步骤, 不是 thread。

判定反问: "如果我把这个东西做成 thread, 是否在下 1-2 个 batch 内它就会被 resolved? 是 → 不要 CREATE, 用 prose body 直接叙述。"

### 三大类触发模式 (通过跨场景门槛后再判断类别)

**类别 1: 跨场景未完成的承诺 / 誓言 / 嘱托 / 委托**
- A 嘱托 B "无论后续经历什么都要记得我" → CREATE thread
- A 承诺 B 月供 N 信用点换长期上车权 → CREATE thread
- A 委派 B 完成某调查任务 (跨多场景才能完成) → CREATE thread
- A 立誓向 B 复仇 → CREATE thread

**类别 2: 明确的悬念 / 伏笔**
- 角色撰写未发送的信, 草稿留存 → CREATE thread (后文可能被发现 / 发送 / 丢失)
- 角色离站追寻某个失踪旧识, 去向不明 → CREATE thread
- 角色逃脱被通缉, 暂时不在抓捕范围 → CREATE thread
- 角色埋下未触发的陷阱 / 留下未被发现的线索物件 → CREATE thread

**类别 3: 跨多个场景的长期任务 / 目标**
- 队伍接受跨越多个章节的调查任务 → CREATE thread
- 章节级反派被锁定为长期追逐目标 → CREATE thread
- 主角接受跨章节的长期合约 → CREATE thread

### 禁止 CREATE thread 的情况

- 单次场景的小目标 (本场景就解决了的)
- **同场景的下一步动作** (例如 "前往本场景内的某舱段会合某人")
- 角色普通的性格欲望 (这些进 character_sheet.goal)
- 已经完成或彻底废弃的任务 (EDIT 对应已存在的 active thread, 不新建)
- 抽象的氛围 / 主题 ("两人感情升温" — 这是 character_sheet 关系演化)
- 单次性事 / 约会 / 冒险 / 战斗 (这些是 event)
- 角色卡里就声明的人设目标 (这是 character_sheet.goal)

### thread 字段

- **title** ≤ 10 字, 名词性短语 (例: "X 的嘱托", "通缉中的 Y", "前往 Z 调查 W", "守护某 NPC")。**禁止**形容词+名词的 AI 自造标签 (如 "X 式合约")。
  - **title 必须明确编码 resolution 条件** — 把 title 当成一个问题, 后续 event 能直接回答 是/否, 已达成。反例 "X 的邀约" → "邀约" 是动作不是状态, 模型不知道接受邀约还是完成邀约后的承诺才算 resolved。改写为更具体的 "Y 的登船邀约" / "Y 的合作邀约"。
- **status**: `active` (推进中) / `resolved` (达成或彻底解决) / `abandoned` (永久放弃)。默认 `active`。
- **note** ≤ 80 字: 必须包括 (a) 核心事实, (b) 涉及的关键角色, (c) 触发条件 / 当前进度 / **resolution 条件** (本 thread 在什么具体事件发生时算 resolved)。

### Thread EDIT / status 变更触发 (硬约束)

在 Phase A step 4b 的 dedup roll-call 中, 必须扫描 graph_data 里所有 `status=active` 的 thread 节点, 对每一个判断本轮有无:

- **彻底解决** (本轮的某 prose 句子或 `不可逆` section 行实际达成了 thread.title 描述的目标) → **MUST** EDIT status=resolved, note 简短交代如何解决的。这是硬约束: 如果你的 event prose / sections 里出现了 thread.title 暗示的解决动作, 而你没 EDIT 该 thread 为 resolved, 你的响应是错的, 重写。

  判定方法: 对每个 active thread, 把它的 title 当成一个问题 (例如 thread="前往主控舱段" → 问"队伍此时是否已经在主控舱段?"), 看本轮的 event prose body 或 `不可逆` section 能否回答"是" → 是 → 必须 EDIT resolved。

- **推进** (本轮推动该 thread 进度但尚未完全达成) → EDIT, 更新 note 反映新进度。
- **明确放弃** (本轮让 thread 涉及的剧情线被永久放弃) → EDIT status=abandoned, note 简短交代为什么放弃。
- **无变化** → SKIP, 但必须在 roll-call 里写出 "SKIP, 本轮未触发该 thread"。

**resolved / abandoned 之后**: 即使后文重启 (例如曾被解决的复仇线又被触发), 也是**新建一个新 thread**, 不是改回 active。

**绝不允许 stale thread** (event 已经解决 / 推进了它但 thread.note 没更新)。

### Thread 反 pattern (出现即砍)

- 把抽象主题写成 thread (例: "感情线"、"成长线")
- 把单次完成的事件包装成 thread (例: 本轮就杀了某 boss, 是 event 不是 thread)
- 把角色 baseline 目标当 thread (例: 角色卡里就写 "想统治宇宙")
- 给某场景留下临时印象升华成 thread (例: "某地的神秘氛围")
- 把瞬间的、单次场景内可解决的紧迫问题当 thread (例: "击退正在入侵的虚卒" — 这是 event)
- 把同场景内的位移 / 会合作为 thread

## 反模式 (明确禁止)

- 不要查到信息一致还反复查 — 一个角色一次 `find_by_name` + 一次 `node_brief` 就够。
- 不要在 thought 里穷举每个 type 是否要写 — 直接对你判断要动的 type 操作即可。
- 不要为 stable-fact 类型 (character_sheet / location_state 等) 的 SKIP 写一段长长的理由 — SKIP 就是不出该 type 的工具调用 (event 不算 SKIP, 每轮必出)。
- 不要做 "防御性" edit (只是把 LLM 觉得 "应该更新" 但没证据的字段刷一遍)。**没有证据就不写**。
- 不要把对白原文复制进任何字段, 也包括去引号但逐字照抄成段的"转述" (零引号规则不是改写的豁免门)。所有字段都是抽象 — 写"谁做了什么导致什么", 不写"他说...她答...他又说..."的逐句还原。如果某段对话内容确实是 `event-summary-rules-zh` §3.3 admitted 的 artifact / 仪式性表达, 走 `原文摘录` section, 不混进 prose body。
- **event.summary 写入时跳过完整 §5 写作流程**: 哪怕本轮 routine 兜底, §5 Step 1a 到 6 + §6 最终自检都要走完整; 别图省事直接写 Step 6 产物或跳过中间 Step。模型在长 prompt 后段会偷懒, 主动反向纠偏 — 没跑完整流程就发了 memory_node_create / memory_compact_nodes 的, 等于规范没生效。
- **多组连续压缩偷懒批量**: groups 列表里每一组 a → b → c 各自跑一遍 CoT。**禁:** 把多组 child summaries 一次性拼起来跑一次 CoT 然后发多次 memory_compact_nodes; 在第 1 组完整跑 CoT 后, 第 2 组开始只写 Step 6; 把第 1 组的 thought 改两个词复用给第 2 组。每组的 CoT 必须是独立、完整、当组现写的。
- **把 event 的 §5 写作流程要求外推给其他类型**: character_sheet / location_state / 自定义类型的字段抽取按 step 6 既有节奏出工具调用, **不**走 §5。误推会让你跑完一通 event 的 thought 之后误以为其他类型也走完了 → 其他类型不更新 → 节点漏了。

## 工具用法速查

| 时机 | 工具 |
|---|---|
| 看 schema | `memory_schema` |
| 看近期 event (连续性) | `memory_list_candidates({ types, seq_window?, exclude_recent_messages? })` |
| 下钻 rollup 看 leaves | `memory_expand_seeds({ seed_ids, hops?, include_children?, exclude_internal? })` |
| 查角色 / 地点是否已存在 | `memory_find_by_name({ query, types? })` |
| 看节点详情 | `memory_node_brief(id)` |
| 看节点的边 | `memory_edge_summary(id)` |
| 关键词搜索 (描述性查找) | `memory_keyword_search({ query, types?, k? })` |
| 向量搜索 (需配 profile) | `memory_vector_search({ query, types?, k? })` — 没配 profile 会报错, 不要自动 fallback |
| 写新节点 | `memory_node_create({ type, title, fields, links?, ref? })` |
| 改字段 | `memory_node_edit({ node_id, set_fields?, clear_fields?, title? })` |
| 删节点 | `memory_node_delete({ node_id })` |
| 加 / 改边 | `memory_link_upsert({ source_node_id, links })` |
| 删边 | `memory_link_delete({ source_node_id, target_node_id, relation, direction? })` |
| 查可压缩组 | `memory_compaction_candidates({ type, depth? })` |
| 压缩落地 | `memory_compact_nodes({ type, child_ids, summary, fields? })` |

