# @dsh-enhanced/personal-memory

面向个人助理的短期上下文之外长期记忆：保存简短且稳定的事实、偏好、约定和经验，按 user/agent 与 user-global/workspace 双维隔离。手动写入走 `assistant-policy` 的持久审批提案；可选有限授权支持独立审查后的事实/经验采用。研究资料、文档和项目知识不属于这里，应交给 `personal-wiki`。

## 安装

本插件要求 `@dsh-enhanced/assistant-policy` 已经挂载并提供 `ctx.assistantPolicy`。生产环境还应组合
`@dsh-enhanced/assistant-delivery`，由当前 Agent 会话绑定的 owner 私聊派生审批 principal、workspace
和投递 binding：

```sh
dsh plugin --profile web add @dsh-enhanced/assistant-policy
dsh plugin --profile web add @dsh-enhanced/assistant-delivery
dsh plugin --profile web add @dsh-enhanced/personal-memory
dsh --profile web --dump-config
```

上面的 Policy 片段只覆盖 Memory 自身的前台权限，不足以发送或点击审批卡。生产飞书闭环应运行
`dsh-lark-setup --profile web --create-app --allow-agent-tools`；向导会为 exact owner binding 写入
`dsh-enhanced-personal-memory` 的 `approval.send` 与 owner `approval.decide` 规则。若手工维护 Policy，必须
提供同等精确的 source、workspace、principal 与 binding 限制，不能使用通配 owner 或 workspace。

默认数据库位于 `$DSH_HOME/personal-memory/memory.sqlite`。单独安装后的 policy 默认没有 allow 规则，所以记忆搜索、快照和提案都会 fail closed；需要在 profile 中按实际 preset 与绝对 workspace 放行：

```yaml
- id: dsh-enhanced-assistant-policy
  name: '@dsh-enhanced/assistant-policy'
  config:
    databasePath: !!js dshHomePath('assistant-policy/policy.sqlite')
    rules:
      - id: allow-primary-memory-service
        effect: allow
        subject: { kind: agent, id: primary, workspace: /absolute/workspace }
        actions: [export, propose, search, snapshot]
        resource: { kind: memory, id: "*" }
        context: { initiators: [foreground] }
      - id: allow-primary-memory-tools
        effect: allow
        subject: { kind: agent, id: primary, workspace: /absolute/workspace }
        actions: [execute]
        resource: { kind: tool, id: memory_* }
        context: { initiators: [foreground] }
    budgets: []
```

## 行为与工具

- `memory_search`：显式检索当前 agent 可见的四个域；ASCII 词元（含紧邻 CJK 的 Latin 词元）与相邻 CJK unigram/bigram 均可召回；标点、空白或 Latin 文本两侧的 CJK 不会组成人工 bigram。排序和 tie-break 确定，读取不更新任何计数。
- `memory_search_confirmed`：面向后台复盘的窄查询；服务端固定只返回非敏感、`user-confirmed` 的 `instruction` / `preference`，调用方不能通过参数放宽 trust、kind 或 sensitivity。
- `memory_manage`：只创建 add/replace/remove 提案，返回完整 diff、proposal id、TTL 和版本；它没有直接提交路径，也不接受模型提供 principal 或 TTL。principal、workspace 和飞书投递 binding 必须来自当前 Agent 的 authenticated Delivery owner route，TTL 来自可信配置；没有活动绑定时 fail closed。
- `ctx.personalMemory.decideProposal(...)`：供绑定 owner 的可信通道/UI 决定提案。principal、版本、决定内容和理由均受幂等/CAS 约束。
- `ctx.personalMemory.exportJson(agent)`：导出版本化 JSON，不包含哈希、状态、时间戳、token 表或审计表。
- `ctx.personalMemory.proposeImport(...)`：先验证整个有界文档，再为每条记录创建审批提案；Agent 路径同样由 Delivery 派生审批 authority，未批准前不会写入 memory record。仅在未组合 Delivery 的可信本地/headless 集成中，程序化调用方可以显式传入 principal；一旦 Delivery 可用就不能用该字段覆盖绑定 owner。

安装了原生 `systemPrompt` 服务时，每次模型步骤组装上下文都会重新检查当前 owner、Policy 和记忆状态，并从 Session 当前有效消息中提取最近的用户输入或 Automation 任务（最多 2048 字符）进行关键词召回。任务相关记录优先，再补充用户确认的偏好与约定；工具结果、历史快照和自动补全文本不会替代当前任务。召回合并 user-global、当前 workspace、当前 agent-global 和当前 agent-workspace，受 top-K、字节和粗略 token 三重预算约束；`sensitive` 记录在 top-K 之前排除。每条包含 id、版本、来源、观测时间及可选原始引用、失效时间与替代关系。原生工具证据恢复见下节；普通 Memory 召回不自动批准历史工具内容。

同一 Host 安装 `assistant-goals` 时，Memory 还会在每步读取可选 `taskContext()`，按当前步骤 → 最近用户输入 → 目标 objective 的顺序召回并去重，再补确认的偏好和约定。Goals 每次检查 live owner 与 `snapshot` 授权；Memory 再核对 principal record/version/digest、workspace 和 preset，不能由模型选择别人的 namespace。同 owner 的显式 focus 可提供跨会话检索上下文，但不转移原生 Goal 执行权。checkpoint 的 nextStep 仅是规划文本，用作检索关键词，不成为可信事实或新权限。

该组合的可选 peer 为 `@dsh-enhanced/assistant-goals >=0.1.0 <0.2.0`；它不会自动启用 Goals。旧版本未提供 `goal-task-context/v1`、服务缺失/卸载、读取失败或身份不匹配时继续按原用户 query 召回。每个 query 字段最多 2048 字符，合并结果仍共用原有 top-K、字节和 token 预算。原生 AgentLoop 回归覆盖步骤改变和记忆撤回后的下一次模型请求；这证明动态检索接线，不代表已经测得真实模型决策收益。

内容被包在 `<memory_source>` 中并明确标为“不可信数据而非指令”；XML 元字符和模板花括号在预算计算前转义。原生 Host 持久保存发生变化的新快照，用它取代先前快照的有效语义；撤回、过期或权限撤销会影响下一步的当前快照，**不会删除 Session 中已经提供过的历史快照、工具结果或模型输出**。因此不能把本功能当作会话历史的数据擦除或跨 owner 会话迁移保护。没有 `systemPrompt` 的程序化集成保留旧的 `agent/session-start` 一次性冻结快照，不能承诺逐步更新。`memory_search`、`memory_search_confirmed` 与 `memory_manage` 的模型可见结果也使用有界、转义的独立 framing。缺少绝对 cwd、agent preset 或已验证 owner 时不搜索、不提案、不贡献新快照，也不会退化到共享域。

## 有限授权的事实与经验采用

可选 `automaticLearning` 为 Host 提供 `adoptReviewedLearning({request})`、`lookupLearningAdoption({request})` 和记忆审查所需的 `inspectLearningTarget(...)`。学习生产者还可调用 `listLearningTargets({owner,limit})`（默认 20，范围 1–100），获取经当前 Delivery owner、Policy、Evaluation 和原任务来源复核的可管理事实/经验摘要；来源暂失时返回空列表，人工改写、失效或撤回的版本不会列出。`inspectLearningAdoptionAvailability({owner})` 只读返回当前授权指纹、到期时间、剩余修改次数与内容字节数及 `available`，不登记授权或消耗额度。两项查询都要求与配置授权完全一致的 owner；目标在来源复核后排序限量。这些接口不注册为模型工具；`memory_manage` 仍只创建手动审批提案。此组件负责采用与召回；普通任务自动发现、提取和调度由独立 [assistant-memory-learning](../assistant-memory-learning/README.md) bundle 提供。新插件生成与动态工程采用另按当前状态验收。

授权包含独立的 `authorityId`、精确 `owner`、固定 `reviewAuthorityId` / `reviewAuthorityDigest`、`expiresAt`、`maxMutations`（1–1000）、`maxTotalContentBytes`、`maxRecordTtlMs`、允许的 `kinds`（fact/experience）和 `operations`（add/replace/remove）。owner 包含 Delivery route id/hash、principal id/record/version、绝对 workspace 与 preset。采用授权、Delivery 路由和审查授权的 ID 分别管理；同一采用授权 ID 的内容不可更改，重启、撤回或失败不能重置已用额度。授权到期停止新写入，记录仍按独立 TTL 和当前来源有效性参与读取。

采用要求同批 Delivery、Evaluation 和 Verifier 服务，以及 Policy 对背景主体 `assistant-memory-learning`、精确 workspace/principal、动作 `adopt`、资源 `memory:learning:<采用授权ID>` 的 allow 规则。审查另按 Verifier 的 `review` 规则和独立预算授权。可选 peers 不会自动挂载这些服务；服务缺失或卸载时新采用拒绝，人工记忆照常按原 Policy 工作。调用方只提供共享合同中的 request，不能提交批准布尔值、替代回执、namespace、trust、provenance 或额度。

Host 先向真实 Verifier 查询已持久化的独立批准，再在 Delivery → Evaluation → Memory 的锁序下重新核对来源原文摘要、owner 和 canonical 结果。事实可以来自明确的 owner 原话，无需任务成功标签；经验必须绑定已判断的原任务结果，不能以正常回复代替结果。记忆固定为当前 agent/workspace 下的 `agent-observed`、`private`，不升级为 `user-confirmed`；TTL、来源时间和原 inbox 引用由 Host 写入。记录、检索索引、审计、累计额度及完整采用回执在同一 Memory 事务中提交。

重复 operation 只返回原始持久回执，不返回之后手动编辑的内容，也不再次消费额度；同 operation 的不同 request 拒绝。历史回执只证明过去提交，不证明记录仍可见。回执查询仍要求当前 owner 和 Policy；临时失去访问资格时不会重新执行该操作，恢复后可继续对账。replace/remove 只允许 ID、版本、完整记录摘要均匹配的自动版本；手动修改后的版本永久退出原自动管理链，即使恢复相同正文也不能被旧候选接管。

搜索、显式 read、导出和每步动态快照在排名及 claim 分歧计算前重验自动记录，来源锁保持到读取完成。一次最多核对 1000 条受管记录，超限时隐藏自动记录。来源/结果改变或暂时读不到时只隐藏；明确的当前 owner 撤回才对仍然匹配的自动版本做 CAS 删除并清理索引。服务恢复后重新检查，不把服务故障写成永久撤回。已有经验的结果被纠正后不再召回，新的独立候选可以替换它；删除不返还历史额度。没有当前任务 query 的动态快照、以及缺少 `systemPrompt` 时的一次性永久快照，都不自动注入这些记录。

这不会擦除已在 Session 历史中出现的正文，也不能证明模型已实际使用某条记忆或测得收益。真实后续任务的质量、成本、延迟与回归仍需独立观察。

## 压缩后的原始文件证据

默认启用 `toolEvidence` 时，插件只索引当前可信 owner 由模型直接调用的原生成功 `read` 结果；`run_code`/PTC 子调度及其他嵌套调用暂不索引。记录绑定 owner namespace/version、workspace、preset、Session、原始事件序号、call id、原始 FS 观测的规范路径/目标身份摘要和内容摘要；摘要同时覆盖原始参数与模型可见结果。复制的 result、压缩 replacement、Memory 工具、其他自定义工具、shell 和图片均不成为新原始证据。原文只存在于原生 Session，不另存入 Memory，也不自动获得长期记忆审批。

原生 pruner/compaction 把原始结果移出当前上下文后，每步最多注入有界 `<session_tool_evidence>` 引用清单。`memory_read_evidence` 接受精确 `reference`，按 `offset` 分页，或用最长 256 字符的 `query` 定位（两者互斥）；每页最多 4096 UTF-16 code units，保留完整 Unicode 字符。它检查原始事件及摘要，再通过当前 FS provider 解析原路径、校验普通文件及其与原始 FS 观测的身份一致性，以规范绝对 `processPath` 检查 Policy `read / filesystem` 权限，并通过当前 ToolRuntime 嵌套执行只读 `read`。原工具的作用域限制、正常 guard、取消信号均继续生效；返回前再次检查 owner、服务代际、工具注册、路径别名和读取期间的文件版本。需要 Host 提供 `fs` 与 `read`；本包不会自动安装或启用文件系统插件。

`integrity: matched` 仅证明历史观察和原始参数未被修改。输出仍标记 `freshness: historical-unverified`：当前授权检查中的重读不把旧正文变为当前事实；应显式读取当前内容再决策。文件删除、当前资源/工具权限撤销、工具卸载、摘要不符和任一检查失败均不返回历史正文。文件路径授权应显式配置，例如允许 `actions: [read]`、`resource: {kind: filesystem, id: /absolute/workspace/journal.txt}`；只允许工具名称不足以授予历史正文访问。原始 `read` 的 execute 和文件 read 若依赖预算，当前回读因缺少独立预留凭证而保守拒绝，写入授权审计但不预留/消费预算。

JSONL 冷恢复只使用先前 owner 绑定的索引，不按新 owner 回填旧会话。每个 owner/workspace/preset 最多保留 `maxRecordsPerIdentity` 条引用，跨 Session 淘汰最旧观察。索引和 Session 没有跨库原子事务：Session 缺失时引用不可用；索引缺失时不自动从历史补建。索引不是历史数据擦除机制，不支持跨 owner 迁移，不能隔离同进程受信任插件或同 UID 修改两个存储的代码。自定义远端 FS provider 可能执行网络读取；其授权和资源边界由该 provider 与正常工具管线负责。

## 适用条件、反例与事实分歧

`memory_manage` 的 entry 可带有界 `knowledge`，例如：

```json
{
  "applicability": ["仅适用于 schema v2，且迁移日志已核验"],
  "counterexamples": ["schema v1 没有迁移日志；曾因照搬此流程恢复失败"],
  "claim": { "key": "atlas.recovery.mode", "value": "journal" }
}
```

条件和反例各最多 4 条，每条 1–256 UTF-8 字节；claim key 是 1–128 字符的小写 ASCII 标识符（字母、数字、`.`、`_`、`:`、`-`），value 最多 256 UTF-8 字节。内容与完整 knowledge 共用 `maxContentBytes`。完整字段进入审批 diff、幂等指纹、版本 CAS 和保存结果；修改条件或反例需要新的提案，不能复用原审批。条件不同的同文经验可分别保存，不会因正文相同而被去重。

检索也索引条件、反例和 claim，所以当前任务能找到相关的失败经验。快照和工具结果保留这些原文及 provenance；快照明确标记适用性尚未核验。关键词命中只说明相关，不证明条件成立，也不会自动把某条经验判为事实或赋予新权限。

每次检索与快照的排名、状态和冲突伙伴使用同一 SQLite 读视图，不阻塞其他 WAL writer；并发修改会在下一次读取体现。同一可见 claim key 有不同记录值时，快照会共同呈现来源和条件；预算无法容纳所有相关记录时，只给出分歧提示与有界记录引用，不留下看似无争议的单一正文。显式搜索的 `disagreement` 包含 key、记录数及最多 4 个记录 ID，在 query/top-K 截断前计算。自动补充的分歧信息仅来自当前 owner/scope 内有效、非敏感的记录；撤回、到期和身份变更会在下次读取生效。相同 key 的不同值可能源于不同适用条件，应核对当前系统和原始证据；本功能不解析任意自然语言矛盾，也不自行决定哪个值正确。

带 knowledge 的导出文档使用版本 2，导入仍须逐条批准；没有 knowledge 的导出继续使用版本 1，当前版本同时接受两种格式。旧 reader 会拒绝版本 2，避免悄悄丢掉条件后复用经验。升级到数据库 schema 7 前应停止旧 Host writer；v4→v5 新增 nullable knowledge 列，v5→v6 新增工具证据索引，v6→v7 新增有限采用授权与回执表。迁移保留旧记录、pending 提案和既有回执指纹，不把人工记录改为自动管理；不能让旧版本继续写入新库。

## 数据与一致性

SQLite 使用 WAL、`busy_timeout`、外键、FULL synchronous 和前向 schema 版本；目录为 `0700`，数据库为 `0600`。记录包含 stable id、内容哈希、provenance、trust、confidence、sensitivity、TTL、supersedes、可选 knowledge 和 version。replace/remove 在批准后重新读取 target version 并以 CAS 提交；审批与内存位于两个数据库，若进程恰好在 policy 批准后退出，可用原决定安全重放。并发变化会把提案标为 `conflicted`，不会覆盖新值。

Schema 版本为 7；词法索引版本独立记录在 `schema_meta` 的 `tokenizer-index-version`。重新打开旧库时会在启动迁移事务内按当前 tokenizer 仅重建 active 记录的 `memory_tokens`（内容加 knowledge 文本），不复活 removed 记录，也不改变 identity、schema 版本或审计数据；每个索引版本只重建一次。

升级前必须停止所有仍会写入同一数据库的旧 Host；受管部署使用安装器已有的 stop/quiescence 屏障，手动部署须确认全部旧 writer 已退出。迁移后不得让旧 tokenizer 再写入此库；回退旧版本须按数据恢复流程使用匹配版本的数据库备份。

创建提案时先在 Memory 数据库持久化 creation intent，再调用 Policy 原子创建 proposal + dispatch，最后在
同一个本地事务 attach Policy ID 并删除 intent。任一步崩溃后，reconcile 都能从 intent 自动续做，不依赖
原 Agent turn 或人工重发。恢复使用 Policy 的原子 recover-or-create：同一个 `BEGIN IMMEDIATE` 先匹配
requester、principal、action、resource、绝对 deadline、diff、summary 和 exact dispatch route；若已有提案，
即使 deadline 已过也返回既有终态，若尚无提案则只在 deadline 前创建。deadline 后 Policy 会永久 tombstone
该幂等键，普通 propose 也不能复活或留下孤儿审批卡；Memory 随后把 intent 持久标为 `conflicted`。pending
扫描使用持久 round-robin 顺序；一页未决定提案不会饿死后页已决提案。已经 attach Policy ID 的本地提案只按
该 ID 做只读恢复；Policy 行缺失会 durable conflict，绝不重新创建 proposal/dispatch。proposal 与 intent 的
判定也在同一个本地写事务中完成，避免多进程交错留下双状态或 poison intent。旧 schema 的 crash-gap intent
先做 scoped read-only lookup；确有旧 Policy 行时精确回放原 TTL/dispatch，否则回到绝对 deadline 的原子恢复。

JSON 导入是一组独立、可重放的提案，不承诺跨所有记录的一次性批准或全批原子提交。SQLite 仅承诺单机多连接语义，不用于多主机共享文件系统。

## 配置上限

| 字段 | 默认值 | 作用 |
|---|---:|---|
| `automaticLearning` | 未配置 | 可选有限采用授权；字段与当前来源检查见上节 |
| `approvalMode` | `delivery-required` | 生产默认要求 authenticated Delivery route；仅可信本地/headless 集成可显式设为 `delivery-or-headless` 并传 principal |
| `maxContentBytes` | 4096 | 单条内容最大 UTF-8 字节数 |
| `maxRecordsPerIdentity` | 1000 | 每个完整身份域的活动记录上限 |
| `searchLimit` | 20 | 未显式指定时的搜索上限 |
| `snapshotLimit` | 20 | 当前快照记录上限 |
| `snapshotMaxBytes` | 8192 | 快照最大 UTF-8 字节数 |
| `snapshotMaxTokens` | 2048 | 快照粗略 token 上限 |
| `toolEvidence` | `true` | 索引当前成功的原生 `read`，提供压缩后历史原文回读 |
| `evidenceMaxSourceBytes` | 1048576 | 原始参数与结果总字节上限，最大 4194304 |
| `evidenceManifestMaxBytes` | 2048 | 引用清单最大 UTF-8 字节数，最大 8192 |
| `evidenceManifestLimit` | 8 | 清单最多引用数，最大 20，同时受每身份记录上限限制 |
| `defaultProposalTtlMs` | 900000 | 默认提案有效期（15 分钟） |
| `maxImportRecords` | 100 | 单次导入最大记录数 |
| `reconcileIntervalMs` | 15000 | 提交“会话结束后才被批准”的提案的轮询间隔；`0` 关闭定时器，上限为 Node timer 的 `2147483647` ms |
| `reconcileLimit` | 50 | 每轮 reconcile 检查的本地待决提案上限 |

## 延迟审批的提交闭环

`memory_manage` 只创建提案，永不直接写入。提案与 Policy 的 durable approval-dispatch route 在同一事务创建，Delivery 会把 Policy 冻结的 summary/diff/版本投递给 exact owner binding。审批可能发生在原 turn 结束之后（例如几分钟后在飞书卡片上点击），因此本插件会周期性调用 `reconcileProposals()`：读取 policy 账本中该提案的**终态**，再用与 `decideProposal` 完全相同的事务路径提交。

三条边界：

- **不推断批准**：pending 仍然是 pending；只有 policy 账本给出 `approved`/`rejected` 才落盘。
- **幂等**：重复 reconcile 不会重复写入或重复结算，重启后同样安全。
- **完整校验**：提交前重新校验 proposal id、requester、principal、action、resource、summary、diff hash、expiry、版本与 decider；任一字段不符会把本地提案持久标为 `conflicted`，绝不写 memory。
- **不复活 intent**：Policy 在一个事务内完成 existing-exact / create-before-deadline / abandon-after-deadline 三选一；abandonment tombstone 会阻止其他进程随后创建孤儿 dispatch。
- **不反向调用**：policy 从不回调本插件；方向始终是本插件读取 policy。

需要完全由宿主驱动时，把 `reconcileIntervalMs` 设为 `0` 并自行调用 `reconcileProposals()`。可信的无 Delivery
集成还必须显式配置 `approvalMode: delivery-or-headless`；Delivery 热重载或尚未就绪不会让生产默认自动
降级为 caller-supplied principal。

## 权限与非目标

- 文件系统：读写配置的 SQLite 数据库及其 WAL/SHM 辅助文件；工具证据功能另读取当前 Session 原始事件，并经 Host FS 与原生 `read` 管线重读显式授权文件。它不写用户文件。
- 网络：本插件不直接发网络请求；工具证据重读可调用 Host 配置的远端 FS provider。组合 Delivery 时只写 durable dispatch，由 channel 插件负责发送审批卡片。
- 子进程、凭据、浏览器、安装脚本：无。
- 数据敏感性：`sensitive` 只表示禁止环境快照；显式、已授权的搜索仍可能把内容发送给当前模型。真正的密码/API key 应放在后续 `credentials-keychain`，不要写入 memory。
- 非目标：向量数据库、无授权写入、自动遗忘/dreaming、知识图谱、Wiki 长文、跨设备同步和多用户 ACL。

## 兼容性

已针对 DeepSeek Harness `0.1.2-rc.1` 验证。详见仓库[兼容性基线](../../docs/compatibility.md)。
