Skip to content

日记本 v2:一个 Agent 一本,素材取全局记忆 #613

Open

Description

@modusensus

目标

侧边栏新增「ta的日记」页:一个 Agent 一本日记,导航栏按 Agent 名切换;手绘日记本风格,Agent 可自选纸样与标题;素材取全局记忆(该 Agent 在各会话里留下的痕迹),而非只读当前会话转录。

前置(2026-11-08 实测校正,已含 rebase 后的分支)

原先写「前置 = #606 / PR #609」不完整,日记本体根本不在 main 上。真实顺序三步:

第 1 步(已就绪,等落地):feat/diary-v013-rebased,worktree .worktrees/qq-diary,HEAD 360bfed。

  • 基于 7461768(当前 origin/main,Merge PR feat(panel): 用户设置块产出段边界,面板可按段读 #601)重做后的三笔:a82e03d 日记 v0.13(35 files, +3530)→ d698d36 页签入口 → 360bfed M.O. 下架。
  • 相对 origin/main 35 files, +3681/−40,工作区干净,git rev-parse --verify origin/feat/diary-v013-rebased 报 fatal: Needed a single revision → 未 push、无上游、无 PR。
  • src/diary/ 8 个文件(auto / compose / dir / export / index / qq-inbox / rollover / session-provider)只在它上面;git ls-tree -r origin/main -- dsh-mneme/src/diary 为空。lib/diary/ 同名 8 个文件与之逐 blob 一致(8/8 OK)。
  • 验证:check-sync 103 个文件一致(比 main 多 7,正是日记那批);全量 2930 tests / 2929 pass / 0 fail / 1 skipped。
  • 原始分支 feat/qq-diary(父 041e8f3,落后 main 9 提交)从未 push,origin/feat/qq-diary 不存在。

第 2 步:#609(#606 bySession)合入。git grep -c bySession origin/main -- dsh-mneme/src → exit 1、零命中。没有它,agentId 只剩标题前缀一条腿。

第 3 步:从「main + v0.13 + #609」切 feat/diary-by-agent。

若从当前 main 直接切分支,src/diary/ 整目录不存在,写出来是一份孤儿实现,rebase 时全废。

rebase 冲突面复核:git merge-tree --write-tree origin/main feat/qq-diary 只报一个冲突文件 dsh-mneme/CHANGELOG.md。main 自 041e8f3 以来只动 4 个文件(CHANGELOG +15、src/user-settings.js、lib/user-settings.js、test/user-settings-segments.test.js +528),与 src/diary/ 零交集。

CHANGELOG 解冲突的隐蔽坑(不是代码问题,测试也不管):第一轮解完,main 的 #600 段化条目被并进 ## 🎨 面板 节下,还多出一个空「面板」节。重做后已复原:[Unreleased] → 🆕 新增(日记 v0.13 + #600)→ 🎨 面板(M.O. 下架)→ [0.12.0],diff 是 +20 行、0 删除。这类错位不报错但会让后来的人以为 #600 是面板改动——以后 rebase 值得专门看一节归属。

本 issue 不动 #603 的 worktree。

决定性的四条实测事实

这几条都从 live 库(约 273MB)现查,不是推断。

1. sessionId 不是 Agent 的键

按 memories.source 里的 sessionId 分区是错的。实测同一个 bot 名下 24 个会话(13 个「Amarre · 记忆研究」+ 11 个「Amarre」),另一个 bot 4 个;近三天每天 23/36/27 个不同 session 源。按 sessionId 分区,一个 bot 会散成 N 本,与「一个 Agent 一本」正相反。

正确的分区键来自宿主标题事件。ctx.sessionProjections 是注册进 cordis 服务表的正式服务,插件可以直接注入(宿主自己的 dsh-action-cards、AgentLoop、agentPresets 都这么做)。但 projection 里只有标题字符串、没有 source:titleProjectionDefinition.apply 是 event.type === "session/title" ? event.data.title : state。要判「用户手改」还是「模型自动起」,必须读事件日志 session.snapshotEvents().findLast(e => e.type === "session/title").data.source。

实测 707 个会话目录:source: "user" 的标题全是「Bot 名 · 主题」形(Amarre · 记忆研究 / Ephor · 记忆研究 / Loom …),前缀无一例外;source: "fallback" 是整句(「可以生图吧?……」「@专家团:……」)。所以规则是:source === "user" 才取「·」前缀,且前缀必须命中已知 bot 名单;fallback 落 _unnamed。

2. source 有两种形态,且零重叠

  • session:session-<uuid> 6492 条
  • 裸 session:<uuid> 1146 条

同一 uuid 不会两种并存(INTERSECT = 0)。今天 top 10 会话里有 3 个只有裸形,只用 LIKE 'session:session-<id>%' 会各漏 7–8 条,且不报错。正确写法 source IN ('session:session-<uuid>', 'session:<uuid>')。另有 97 条别形(session:onebot-private-* 95 条 + 2 条自由文本脏数据)。

3. 日界必须按本地日,差一倍

created_at 存 UTC。2026-11-08 当天 session: 行:UTC 裸判 105 条,本地日(datetime(x,'+8 hours'))210 条。今天按完整口径是 created 242 / 修正后 235。SQL 必须 datetime(x, '+8 hours'),不能用 date('now') 也不能取 created_at 前 10 位。

occurred_at 空值占活跃行的 54.4%(4429 条),COALESCE 是必需项。

4. occurred_at 有一批整 8 小时的偏移

偏移分钟直方图是尖峰不是散布:偏移 0 801 条、-480 705 条、-481 73 条,[-490,-470] 全带命中 854 条(活跃口径;不过滤 archived/forgotten 则 1085)。这 854 行全部是 .000Z 整秒精度(全库整秒 3471 条、带毫秒 246 条),即来自外部批量写入而非本机生成;整带 +8h 后与 created_at 的最大偏差降到 10.3 分钟,是单向可复原的错误。月份集中在 10 月 765 / 11 月 89。

判据必须按偏移量认,不能用「相差 >12h 就回落」:那 854 行全部落在 12h 以内,阈值法一条都挡不住;反过来非 8h 带里还有 195 条确实相隔 12h 以上的(真实「回忆往事」型记忆),阈值法会把它们一起误判成不可信。

方案的五个部分

§1 面板接 bySession

lib/client.js:4422-4425 读回 persona 时只认 default/byAgent(isMulti = "default" in pj || "byAgent" in pj);:4647 保存时只发两维。改法是纯加法:isMulti 判定加 "bySession" in pj、保存体加该键,src/settings.js:562 已兼容三键不用改后端。

§2 agentId 解析

三级:命中名单的标题「·」前缀 → _unnamed 兜底 → 拒绝把 fallback 整句标题当 id。「公共管理复习」这类用户手改的标题不能造出同名 agent 目录,否则会冒出一个不存在的 Agent。

§3 存储布局与归并规则

两套路径并存,必须显式分开:

  • 按项目分桶(v0.13 现状):<memoryDir>/diaries/<项目键>/YYYY-MM-DD.md
  • 按 Agent 分桶(本 issue 目标):<memoryDir>/diaries/agents/<agentId>/YYYY-MM-DD.md

不迁移存量项目桶(两者是正交维度),但归并规则必须写进实现,因为按项目分桶这一层已经出过一次「同一项目两个桶」:

§3.1 按 hash 后缀认同一个项目,不按前缀名。 projectKey(src/candidates.js:194 起)取 sha256(projectRoot) 前 8 位作后缀;前缀是 path.basename 过 .replace(/[^A-Za-z0-9._-]/g, "-") 的结果,清洗规则一改前缀就漂,hash 不会。实测 sha256("D:\QQ对话") = 4f061790c57fcb63…,后缀 4f061790 稳定可复算。归并时只认后缀,前缀仅作展示名。

§3.2 同 hash 多桶要合,且不设「真正的那份」。 现场真发生过:QQ对话-4f061790(11-07 一篇、persona: settings (amarre)、无 runs.jsonl)与 QQ---4f061790(11-08 一篇、persona: default (ptc)、有 runs.jsonl)同 hash、同项目。已在 2026-11-08 手工合入后者:先复制、校验 SHA256 一致、再删源(1565B → 1565B),现 QQ---4f061790 有 2026-11-07.md / 2026-11-08.md / runs.jsonl,旧桶已删。当时的现场是 6 桶(amarre-c836db6a / attune-dd7a5103 / dsh-mneme-54f4c9ae / QQ---4f061790 / QQ对话-4f061790 / workspace-e9be35f6),现在是 5 桶。实现要做的是自动化这条合并:同 hash 两桶、日期不重叠 → 各篇归位到同一目录;日期重叠 → 不许静默覆盖,留两份并报审计(哪一份留、哪一份降级为 .conflict-<ts>.md)。

§3.3 runs.jsonl 记账从新桶起算,别指望旧桶补。 旧桶没有 runs.jsonl;新桶那条 ledger 里只有合并后写的一轮({"v":1,"date":"2026-11-08","trigger":"rollover","status":"written","mode":"create","items":6,"source":"session","persona":"default","pointer":"superseded"}),其余是 3 条 skipped。迁移不重建历史记账,合并时把旧桶有的 runs.jsonl 按时间序追加,缺的就是缺,不补写、不伪造。

§3.4 frontmatter 的 project: 是历史记录,路径才是权威。 合过来的 2026-11-07.md 里仍写 project: QQ对话-4f061790(旧的桶名),因为它记录了当时被写出来的样子,不改写历史文件。读取侧一律以目录路径为准;以后 project: 只作为「生成时的项目键」留痕。

§4 素材 SQL

WITH src AS (
  SELECT type, title, content, importance, source,
    CASE WHEN CAST(round((julianday(occurred_at) - julianday(created_at)) * 1440) AS INTEGER)
              BETWEEN -490 AND -470
         THEN datetime(substr(occurred_at,1,19), '+8 hours')   -- 修 -480
         ELSE COALESCE(substr(occurred_at,1,19), substr(created_at,1,19))
    END AS ts                                                   -- ts 已是 UTC 语义
  FROM memories
  WHERE forgotten = 0 AND archived = 0
    AND source IN ('session:session-<uuid>', 'session:<uuid>')
)
SELECT type, title, content, importance, source, ts
FROM src
WHERE datetime(ts, '+8 hours') >= :dayStartLocal
  AND datetime(ts, '+8 hours') <  :dayEndLocal
ORDER BY ts ASC
LIMIT 400

注意 ts 已经是 UTC 语义,外层日界仍要再 +8 hours 一次。

§5 触发与开关

复用 auto.js 的 turn/end 判定 + 启动 catchUp 一次一篇,改 agentId 维度节流;不加固定时刻定时器。diaryEnabled 加到 PERSONA_FIELDS 白名单。

默认值口径别混(两个事实并存,不矛盾):代码里 src/config.js 是 diaryEnabled: z.boolean().default(false)(opt-in 默认关);而 live 库 user_settings 里 diaryEnabled = true(用户自己开过)。不要把 issue 写成「默认开启」,也不要拿代码默认值去解释线上状态——两者说的不是同一件事。本 issue 保持 default(false) 不改,理由同上:这个开关会把日记从「按项目」切到「按 Agent」并开始写新目录,默认开等于用户重启后行为变了却没人告她。

两处会静默出错的坑(必须进验收)

A. 漏写一次 COALESCE,4037 条静默进 null 桶

ELSE 分支里 COALESCE 漏掉时,occurred_at 为 NULL 的活跃行(4429 条)会全部落进 date = null 桶,外层 WHERE 默默滤掉,SQL 不报错、结果还是个正常数字。实测该日条数塌陷:

本地日 正确 漏 COALESCE 丢
2026-10-17 1004 478 526
2026-11-07 489 358 131
2026-10-09 365 7 358

10-09 那条是三者里最有说服力的:丢 358 条、只剩 7 条,结果看上去仍是个正常数字。

B. LIMIT 400 截断的正好是晚上那段

按本地日超 400 的天:10-17 1004、10-12 511、10-16 508、11-07 489、10-18 415、11-06 412。ORDER BY ts ASC LIMIT 400 砍掉当天尾部,11-07 的尾部正是 18 点 96 条、19 点 96 条、22 点 75 条 —— 日记会系统性漏掉她晚上做的事,且不报错(与 #611「图被静默丢」同一病)。

单会话单日峰值实测 219 条(10-09 session-1a22c3a7;10-17 session-aa4d5ef2 同为 219),所以「按 agentId 分流后 400 就够」不成立——那个 52 条是只看当天得出的,不能外推。

处理:① 分流后再卡上限;② 超限落审计 diaryTruncated: {total, used} 并经 diaryAuditPath 记录;③ 日记正文末尾留一行「今天素材 N 条,本篇用了 400 条」。

验收红线

  • 时区:造 2026-11-08T20:30:00Z(本地 11-09 04:30)的行,断言落在 11-09;
  • COALESCE(两条断言):① 结果集里没有任何 date(ts,'+8 hours') IS NULL 的行;② 造一条 occurred_at 为 NULL 的已知行,断言它出现在 created_at 的本地日;
  • -480 修正:造 created_at=2026-11-08T12:21:15Z、occurred_at=2026-11-08T04:20:00Z 的行,断言出现在本地 11-08 而不是 11-07;
  • source 两形:同一 uuid 造两形各一条,断言都进当天;
  • LIMIT 不静默:造 500 条同日素材,断言元信息记了「共 500 条、用了 400 条」;
  • agentId 守卫:标题「公共管理复习」不得造出同名目录,必须落 _unnamed;
  • fallback 标题不认;
  • 冷行默认进 _unnamed;diaryBackfillColdAgents=true 时才解冷行,且它解的是标题的 source.kind 不是 agent 归属,命中不了名单仍留 _unnamed;
  • 同 hash 两桶归并:造 X-aaaa1111(有 day1)与 Y-aaaa1111(有 day2)两桶,断言归并后同目录且两篇都在;再造两桶各有 day1 → 断言不静默覆盖、报审计;
  • diaryEnabled=false 一个字都不写;黑名单类型(diaryMemoryExcludeTypes 默认 constraint/preference)真被排除;
  • 面板 bySession 保存/读回闭环。

不做

多 Agent 共用一本;跨项目转录抓取(宿主无接口,memories 是唯一稳源);Agent 自由生成 CSS(无沙箱);固定时刻定时器;历史项目桶迁移(只做同 hash 归并);改写历史文件的 frontmatter。

已知权衡

素材从会话转录换成记忆库,换来跨项目跨工作区视野,代价是日记偏工作日志:Agent 在群里说的话若没进记忆库,日记看不见。二期可把 QQ inbox 兜底扩成群消息源(onebot-* 的 source 已在库里,98 条)。

Activity

  1. modusensus commented on Nov 8, 2026

    @modusensus
    MemberAuthor

    处置:待定(前置第 1 步还没落地,现在动手会写成孤儿实现)

    判定依据

    issue 自己写的三步前置经我核对成立,而且第 1 步的载体就是我方 PR #614(同一天开的):

    步 内容 实测状态(2026-11-09)
    1 feat/diary-v013-rebased 落进 main 未落地——PR #614 仍 OPEN,head f57d204,8 项必需检查全绿、MERGEABLE,等合并
    2 #609(#606 bySession)合入 未合——git grep -c bySession origin/main -- dsh-mneme/src 仍零命中
    3 从「main + v0.13 + #609」切 feat/diary-by-agent 前置未满足

    git ls-tree -r origin/main -- dsh-mneme/src/diary 为空,即 src/diary/ 整目录只在 #614 的分支上。若现在从 main 切分支做 v2,写出来是一份孤儿实现,#614 一合就全废——这不是排期偏好,是 rebase 面的事实。

    对正文的核对结果

    逐条查过 live 库与源码,四条决定性事实全部成立,没有发现需要修正的地方:

    • source 两形零重叠:session:session-<uuid> 与裸 session:<uuid>,INTERSECT 为空 → SQL 必须 IN (两种),只用前者会静默漏 7–8 条/会话;
    • 日界必须 +8 hours:2026-11-08 UTC 裸判 105 条 vs 本地日 210 条;
    • occurred_at 空值占活跃行 54.4%,COALESCE 是必需项;
    • -480 偏移带共 854 条活跃行、全为 .000Z 整秒精度 → 必须按偏移量认,阈值法(>12h 回落)一条都挡不住,还会误伤 195 条真跨 12h 的记忆。

    两处「静默出错」的坑也复核过:漏 COALESCE 会让 10-09 从 365 条塌到 7 条而结果仍是个正常数字;LIMIT 400 砍掉的正是 11-07 晚上(18 点 96 条 / 19 点 96 条 / 22 点 75 条)——与 #611「图被静默丢」同一病。

    一个补充(正文没写,我认为该进 §2 验收)

    agentId 解析取标题「·」前缀,但标题是用户随时能改的(source === "user" 正是「她手改过」这一档)。建议验收里再加一条:手改标题不得让已存在的 agent 桶换名或分叉——即 agentId 只在会话首次被写入时才落定,之后改标题只更新展示名、不动目录。否则用户改一次标题,那本日记就在侧栏里多出一本、旧的那本变成孤儿。

    下一步

    前置第 1 步(#614)合并、第 2 步(#609)合入之后,从那时的新 main 切 feat/diary-by-agent 按 issue 的 §1–§5 落地,验收按正文 10 条红线逐条补测试。在此之前不动代码。

  2. added a commit that references this issue on Nov 9, 2026
  3. added
    area:panel面板与座位(client 侧)
    triage还没定性,等取证
    and removed on Nov 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:panel面板与座位(client 侧)featuretriage还没定性,等取证

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions