hu568/dsh-plugin-persona-memory

dsh-plugin-persona-memory

DSH 插件:角色设定与长期记忆(角色库 persona.yml/SOUL.md/USER.md + FACT.md/MEMORY.md + JOURNAL.jsonl),注册在宿主组合层,对所有 agent 预设生效,附右侧栏管理面板。MIT。

待分类

@dsh-external/dsh-persona-memory

DSH 的角色设定 + 长期记忆管理插件。替换掉 0.1.5 之前那套「预设级记忆加载器」 (memory / memory-boot / ptc-memory 三个 agent 预设 + router-standard 内嵌的 memory-loader)——那些只在选中对应预设时才注入,新方案注册在宿主组合里, 对所有 agent 预设的每个会话生效。

· 许可证 · MIT,见 LICENSE 安装规格 · github:hu568/dsh-plugin-persona-memory 实测 DSH · 0.1.5-rc.1 ~ 0.2.0-rc.2 peer 要求 · cordis >=4.0.0-rc <5、@deepseek-ai/dsh-tools、@deepseek-ai/dsh-system-prompt(随 DSH 提供) 运行依赖 · schemastery(别名安装 npm:@deepseek-ai/schemastery@3.18.2,从 npm registry 拉取)

安装

# DSH 内置插件管理器(创造模式):
#   plugin_manager  action=install_bundle  target="github:hu568/dsh-plugin-persona-memory"

# 或命令行。桌面端(Electron)profile 由应用独占管理,请改用插件管理器或「设置 → 插件」:
dsh plugin --profile <profile> add github:hu568/dsh-plugin-persona-memory

安装后本包被追加进 profile 的 dsh.profile.bundles,其 cordis.patch.yml 作为组合包层 把 persona-memory 这一行插进宿主组合。profile 在运行中通常即时生效,否则重启一次 DSH。

验证:

dsh --profile <profile> --dump-config     # 应出现以本包名为标题的层与 persona-memory 行

也可以在 agent 里直接 GET /persona-memory/api/state:返回 200 + ok: true 说明面板路由已注册。

卸载:

dsh plugin --profile <profile> remove @dsh-external/dsh-persona-memory

卸载不会删除角色库与记忆文件($DSH_HOME/personas、$DSH_HOME/memory),需要时手动清理。

更新时注意:DSH 通过 profile 依赖差分识别「这次装的是哪个包」。若内容变了但安装规格字符串
没变,会报 ambiguous-install;请先 remove 再 add,或让版本号/文件名变化。

它解决什么

平面 · 位置 · 说明 角色设定 · $DSH_HOME/personas//{persona.yml,SOUL.md,USER.md} · 角色库,同一时刻一个「当前角色」 全局记忆 · $DSH_HOME/memory/FACT.md · 跨会话、跨项目的稳定事实;写入硬上限 100 行 / 8000 字节 长期日志 · $DSH_HOME/memory/JOURNAL.jsonl · 一次性事件/会话笔记,仅追加 + 可检索;不注入提示词 项目记忆 · /MEMORY.md · 随项目走;存在才注入,不自动创建;写入硬上限 8000 字符 会话快照 · $DSH_HOME/memory/snapshots/.json · 首条消息时固化的注入内容

注入的东西(提示词段落,顺序紧贴部署人格之后):

  • memory:soul → `` + 角色编辑契约
  • memory:user → ``
  • memory:facts → `` 全局 + 项目 + 记忆写入契约
段落名为什么用 `memory:` 前缀:用户自建的 router-standard 预设会在精简模式里
剥掉除 memory:* 外的全部段落。沿用这个前缀,角色与记忆在那种模式下也能存活,
不需要改预设里的剥离逻辑。
为什么都设 `interpolate: false`:宿主 @deepseek-ai/dsh-system-prompt 的
renderPrompt() 会对未标记的段落做 {{变量}} 插值,而 interpolate() 对
未注册的名字是抛错(不是原样保留)——它认为「malformed prompt 比响亮地失败更糟」。
本插件的段落内容全是用户可写的自由文本(FACT.md、SOUL.md…),
只要哪篇记忆里写了一句 {{变量}},整个 turn 就会以
malformed prompt variable reference "{{变量}}" in section "memory:facts" 失败。
因此这些段落一律 interpolate: false:内容原样进提示词,`{{…}}` 不再有特殊含义。

长期日志:不注入,只按需检索

JOURNAL.jsonl 是无上限增长的流水账。0.1.x 曾把最近 6 条注入 `` 段落, 0.2.0 起整段移除,代价与收益:

· 0.1.x(注入最近 6 条) · 0.2.0(不注入) 每轮 token · 每轮都为最近 6 条付费,随文件增长 · 0(快照里也不存了) 内容相关性 · 最近几条常常与当前任务无关 · 由模型自己判断要不要查 快照体积 · 随日志增长 · 恒定 取旧笔记 · 自动到手 · 要模型主动 memory action=search

不至于「忘了自己有历史」:记忆契约里留了一行指针,写明日志有多少条、正文未注入, 并明确要求模型在觉得「以前可能聊过 / 踩过坑」时主动 action=search。子代理契约里也有一行。

日志的写与读能力完全保留:memory 工具的 append / search / read、 面板的「日志」标签页、/api/journal/{append,search} 路由都照旧可用。 变的只是「不再自动灌进上下文」。

旧 profile 里若写了 journalEnabled / journalInject / journalLimit,删掉即可 ——
schemastery 对未知键是静默保留不抛错,留着也不会让插件起不来(实测)。

写入体量硬上限(全局行数 + 字节,项目字符)

三个平面各有闸口,都在写入侧卡住(超限抛错拒绝,不落盘,绝不静默截断):

文件 · 闸口 · 默认 · 依据 全局 FACT.md · 行数 AND 字节(UTF-8) · 100 行 / 8000 字节 · 注入每个主会话,最坏开销要可预测 项目 MEMORY.md · 字符 · 8000 字符 · 与注入侧 maxChars 同口径 长期日志 JOURNAL.jsonl · 无上限(有意) · — · 不注入提示词,是流水账的去处

全局 FACT.md:100 行 AND 8000 字节

FACT.md 是所有项目、所有主会话都要注入的提示词段落。项目专属结论一旦写进来, 就变成每个项目、每一轮都要付的固定开销,还会把真正「跨项目稳定」的事实挤出去。

只卡行数是不够的:行长完全自由 ⇒ 100 行可以写 7 KB,也可以写 20 KB。 0.4.0 起两个闸口同时生效,任一超限即拒写:

入口 · 行为 memory action=update, scope=global · 行数 > maxFactLines 或 字节 > maxFactBytes ⇒ 抛错拒绝 面板 POST /api/memory/save(scope=global) · 同一道闸口,避免从面板绕开 memory action=read · 报告「当前行数 / 上限,当前字节 / 上限」,已超限时附警告 记忆契约(进提示词) · 事先写明双上限,并要求「本工作区的内容一律写 scope=project」

为什么 8000 字节:本机实测真实文风约 75.9 B/行(4,100 B / 54 行)⇒ 80 B/行 × 100 行 = 8000 B 是贴合真实文风、略留余量的取值。满配约 ≈2,560 tokens (实测换算比 0.3205 tok/字节),把最坏情况钉住。

字节口径:UTF-8(中文 3 字节/字),且先做 \r\n → \n 归一化 (与 countLines() 同口径,不把 \r 算进去)。

项目 MEMORY.md:8000 字符(补上原本缺失的一侧)

0.4.0 之前这里是不对称的:

侧 · 0.3.0 行为 注入侧 · 有闸口 —— maxChars 默认 8000,超出被 clip() 静默截断 写入侧 · 完全没有 —— scope=project 的 update 不校验体量

⇒ 「写的时候不拦,读的时候悄悄截断」:文件里会有一大段内容模型永远看不到, 而写入时毫无警示。这正是本插件明确反对的模式(静默截断等于悄悄丢用户内容), 此前只堵了 global 一侧。0.4.0 补上:

入口 · 行为 memory action=update, scope=project · 字符 > maxProjectChars(默认 8000)⇒ 抛错拒绝,并指向 action=append 面板 POST /api/memory/save(scope=project) · 同一道闸口 memory action=read · 报告项目记忆的「当前字符 / 上限」,已超限时附警告 主会话契约 + 子代理契约 · 都事先写明项目记忆上限

单位用字符,与注入侧 maxChars 逐字同口径(都取 trim() 后的 JS 字符串长度, \r\n 按 2 个字符算、不做归一化)⇒ 「通过写入闸口」即「必然不会被注入侧截断」, 边界正好落在 8000 与 8001 之间。中文按字符算(3000 个汉字 = 3000 字符)。

JOURNAL.jsonl 故意不设上限:它是无上限增长的流水账、且不注入提示词, body 不进上下文 ⇒ 加限制只会逼用户丢历史。要写长内容就往这里写。

注入作用域:子代理只拿项目记忆

段落 · 主会话 · 子代理会话 memory:soul / memory:user(角色) · ✅ · ❌ 不注入 memory:facts → 全局 FACT.md · ✅ · ❌ 不注入 memory:facts → 项目 MEMORY.md · ✅ · ✅ 注入 长期日志 JOURNAL.jsonl · ❌ 一律不注入(0.2.0 起) · ❌

理由:子代理拿到的是一份具体委派任务。角色人设、跨项目全局事实、历史流水账 都与该任务无关,灌进去只会让子代理的上下文变宽、更容易跑偏;项目记忆才是它需要的 那部分(本仓库/本项目的约定与事实)。

判定「是不是子代理」读的是宿主真实写入的 session.header:origin === 'subagent' 优先,另外 parentSession 存在或 delegationDepth > 0 也认(兼容只写血缘的旧日志 与外部 provider)。宁可判多,也不要漏判后把主会话的角色灌进委派任务。

子代理只注入项目记忆时,写入契约也换成项目版:明确告诉它「全局记忆与角色未注入」, 并允许它只读地 action=read / action=search 去查全局记忆,而不是以为不存在。

想恢复旧行为(子代理与主会话注入完全相同)就设 subagentProjectOnly: false。

注入时机:首条消息快照冻结

  • 首条用户消息之前:每轮组装直接读盘 → 「先建会话 → 改角色/记忆 → 再发第一条消息」拿到最新内容。
  • 首条用户消息之后:把角色 + 记忆固化成本会话快照(内存 + 落盘),此后每轮读快照。

于是 KV 缓存前缀绝对稳定,会话中途改文件不影响它,新会话才看到更新。 快照里不含日志正文(0.2.0 起),只留一个 journalTotal 计数供契约措辞使用。

  • 子代理会话的快照只留内存、不落盘(防止 snapshots/ 随委派次数膨胀)。
  • 快照落盘让 resume / 进程重启后语义不变。

工具

两个工具都是全局注册,没有路径参数,走进程内 node:fs(不受文件沙箱策略影响):

memory

action · 作用 read · 读全局 FACT.md + 当前项目 MEMORY.md + 最近日志(两者都报告「当前体量 / 上限」) update · 整篇覆盖写入。scope=global → FACT.md(超 100 行或 8000 字节直接报错);scope=project → /MEMORY.md(不存在则创建,超 8000 字符直接报错) append · 向 JOURNAL.jsonl 追加一条 {ts,tags,text} search · 按子串(可选 tag)检索日志,新→旧

persona

action · 作用 list / current / show · 列角色 / 看当前角色 / 读某角色三件套 use · 切换当前角色(新会话生效) create / update / delete · 新建 / 改 / 删角色(当前角色不允许直接删)

技能:persona-creator

persona-creator(按访谈流程把一个角色写成三件套 + 质量检查清单)不作为技能注册, 而是作为物料安装到角色库——角色库是模型和人都能看到的地方,指针写在 personas/README.md:

仓库  skills/persona-creator/SKILL.md                        # 正本,在 git 里维护
          │  安装 = 拷过去(一次性步骤,见下)
          ▼
角色库 $DSH_HOME/personas/skills/persona-creator/SKILL.md
       $DSH_HOME/personas/README.md                          # 以相对路径 skills/persona-creator/SKILL.md 指向它
  • 安装(一次性,随版本手动同步):

Copy-Item skills\persona-creator\SKILL.md $env:USERPROFILE.dsh\personas\skills\persona-creator\SKILL.md

  • 本包不注册该技能;package.json 的 files 也不含 `skills`——正本只在仓库,不进 tarball,

所以磁盘上只有「仓库源码」和「角色库」两份(lib/index.js 里那段 register 已删,勿再加回)。

  • 发现通路:persona 工具的描述里写着「角色管理请走 $DSH_HOME\personas」⇒ 模型顺指针进角色库 ⇒

读 personas/README.md ⇒ 找到本技能文件。

右侧栏面板(0.5.0 视觉重做)

「角色 / 记忆」标签页(会话标题右侧的「角色」按钮打开):

  • 角色:列表、切换当前角色、编辑 name/description/SOUL.md/USER.md、新建、删除;角色数 > 4 时出现筛选框,卡片右侧可上移 / 下移排序
  • 记忆:全局 FACT.md 编辑;项目 MEMORY.md 编辑(按本进程见过会话的 cwd 发现的项目)
  • 日志:最近条目、追加、检索(标签可点即搜)

视觉:一份注入的样式表,不再堆内联 style