Repository navigation
Releases
v6.0.0 — 可用:整套落到一台 Windows 电脑上
本页会随勘误就地更新。规范全文以
main为准。
这一版只回答一个问题:这套东西,现在能真的用起来了吗?
能了。不是"设计写完了",是每天在跑、满了自己处理、处理完自己把规矩装回来、出事自己修的那种能。
所以这一版没有新机制——它把前面一块一块交付的东西串成一台能过日子的机器(而且不再需要手动换窗口:同一个对话里,满了自己处理、处理完自己把规矩装回来),
并第一次把平台写死:Windows 10/11 端可用。
本页只讲我们做了什么、为什么这样做。不讲实现手段,不贴代码,也不涉及任何第三方程序的修改或绕过。
一、设计思想:五条从第一天到现在没变过的话
- 人格是数据,不是权重。 换模型是换引擎,换硬件是换车身;身份、人格与记忆才是本体。
- 记忆是可迁移的文件,不是任何平台的附属。 原文归档是最后的证据,索引只是"再算一遍"的产物;
索引可以删了重建,原文不行。 - 该醒才醒。 心跳只管"还活着";醒来只做三件:醒 → 报 → 领。叫醒它的一共三档,
每一档都回答得清"是谁决定的"。 - 到线才动,动前先落盘。 提前清等于凭空丢掉还有用的上下文;没落盘就先动手,等于拿记忆赌运气。
这条在实现里是一道过不去的闸,不是文档上的一句劝告。 - 后台干活,不抢前台。 前台在聊,后台在干;先回一句再干活;宁肯排队,也不打断正在说话的人。
还有一条贯穿始终:不依赖云服务,也不修改别人的程序。 采集、索引、检索、交接、水位、调度、托盘全在本机;
断网时唯一停的是"从网上拿新内容"和"请第二个模型说两句",其余照旧。
二、步骤:一条链路,从开机到一整天过去(同一个窗口,不换)
开机 / 登录
│
├─ 采集在后台待命(不需要你打开任何窗口)
├─ 常驻程序起来(托盘图标变绿,之后每 15 分钟自查一次)
│
├─ 新内容进来 ──→ 归档原文 ──→ 一级索引 ──→ 分块索引与向量
│ │
│ 检索:关键词 + 语义,两路融合
│ 任何一条都能回到「文件:行号」
│
├─ 心跳 ──→ 醒(有活吗)──→ 报(读了什么、水位多少)──→ 领(认领任务,带凭证)
│ │
│ 领不到就排队:节点有拒绝权,任务不丢,只会变旧
│ (变旧必须被看见 → 有熔断)
│
├─ 定期量水位 ──→ 到线 ──→ 先把原文落盘并自证 ──→ 再让底座压
│ │
│ └─ 落不了盘 → 这轮不动(硬闸)
│ └─ 落盘了 → 压(实测 359K → 121K)
│ │
│ 压完把规矩装回来:硬条款重新交给当前会话
│ (你在打字就排队;排上的算欠账,下一轮必补)
│
└─ 每天 / 每次开机 ──→ 六项自检(水位/任务/告警/采集/备份/索引)──→ 有问题就报,不静默
三、要实现什么:目标 → 我们的机制 → 什么叫做到
| 目标 | 我们的机制 | 做到什么算完成 |
|---|---|---|
| 记忆进得来 | 按"内容真的变了"触发采集;读不到就重来,绝不回一句"没有新内容" | 后台完成,人不用守着;失败必须说清"是没拿到" |
| 记得住、找得回 | 原文归档 + 一级索引 + 分块索引与向量,检索走两路融合 | 任意一次检索都能回到 文件:行号;删了索引能重建,条数一进一出 |
| 不换窗口也不丢 | 满了自己处理、处理完把规矩装回来,对话原地继续;交接包退为兜底 | 一次会话能连着过一整天,不需要人手动换窗口、也不需要复述 |
| 满了自己处理 | 到线才动、先落盘再动、动手前不打断人 | 无人干预就能过去;落盘与原文可自证一致 |
| 压完不失忆 | 独立进程按水位带把硬条款装回来;冲突就排队,欠账必还 | 压缩后自动收到重载;被打断的那次不会石沉大海 |
| 它知道该干活 | 三档唤醒 + 醒/报/领三段 + 双次校验凭证 | 没读到该读的就领不到活;拒绝会被记录,不是无限重问同一个节点 |
| 出事能自己起来 | 看门狗 + 每日六项自检 + 告警落盘 + 看得懂的日志 | 进程掉了能自己回来;日志不乱码,一看就懂 |
| 人还在位 | 删记忆、改内核、换底座、迁移合并、改身份、改交接格式——六件事必须人点头 | 任何一条都不会被"顺手做了" |
四、细节:按职责讲,不按文件讲
为什么这样讲:这一版的价值不在某一份实现,而在每块各管一件事、彼此不越界。所以下面每组只回答"它凭什么这样做"。
记忆:原文是最后的证据。 归档不动原文,索引只是"再算一遍"。这条决定了后面所有"压缩、清理、维护"的边界——
它们只动派生数据,绝不动记忆本身。
交接:现在是兜底,不再是日常。 锁定段是"不该变的",自动段是"现场状态";自动刷新只覆盖自动段,
所以既丢不了也不会膨胀。
压缩:我们只决定时机,不实现压缩。 压缩是底座自己的能力,我们负责在正确的时候让它发生。四条纪律缺一不可:
到线才动、不连发(压完读数要过一会儿才回落)、先落盘再动(硬闸)、不打断人。
"发了但没降"会被记下来,不静默。
重载:压完必须把规矩装回来。 摘要里不会有仲裁梯子和红线,所以压完要重装一次。
按水位带不按时间——记忆散掉跟带进来多少相关,跟过了几分钟无关。两条冲突保护:不抢人、不撞正在进行的一轮。
排队不是丢:队列自己就是一档触发,欠账优先于水位带。
唤醒与调度:醒、报、领,各有各的凭证。 醒来先重读该读的东西(凭证是内容指纹),报到要带上现场状态,
领活要过两次校验:派发时读过没有、接活时读的是不是比派发更新。节点有拒绝权,我们这边只剩两条路:
换一个,或者先记着;记着的那条必须能被看见。
自修:报得出来才算活着。 看门狗只管"进程在不在",自检管"活得好不好"(六项),日志管"出了什么事"。
人的位置: 能自己定的不问,该人点头的一样不少(见上表最后一行)。
为什么是 Windows 端。 这套东西的每一根钉子都钉在 Windows 上:通知区、计划任务、本机输入与本机浏览器环境。
所以官方说法只有一句:Windows 10/11 端可用。换平台不是改配置,是重做这几根钉子。
五、证据:真跑出来的数字
| # | 证据 | 结果 |
|---|---|---|
| 1 | 满了自己处理 | 水位 359K → 121K,全程无人操作 |
| 2 | 处理完自己装回规矩 | 一次重载 867 字;被人在打字打断过,队列补发成功 |
| 3 | 活不会丢 | 调度自证 PASS 5 / FAIL 0(含"拒绝—排队—重派") |
| 4 | 采集不用守着 | 后台完成,不需要你打开任何窗口 |
| 5 | 每天六项自检 | 六项全 PASS |
| 6 | 整体自证 | PASS 36 / FAIL 0 |
| 7 | 日志可读 | 日志编码扫描 44 个文件,0 个异常 |
六、边界:先说没有什么
- 只有 Windows 10/11 端:没有 Linux/macOS 实现,也不打算为它们改。
- 只配合一个底座:水位读数、压缩能力都来自你正在用的那个客户端;换底座要重接。
- 单人单机:多设备同步只写清了"怎么读写不破坏文件",没有做成同步服务。
- 换窗口已不是常态:靠交接包续接的那套动作已降级为灾备;日常不需要换窗口,也就没有"换窗口后要复述"这一步。
- 不修改、不绕过任何第三方程序与鉴权:我们不破解、不伪造身份、不绕登录;
需要你本人已登录的地方,过期就要你手动登录一次,采集本身不重试、不刷频率。 - 摘要实体校验没做:压完靠重载补规矩,不校验摘要里的事实有没有错。
- 断网兜底没有真断网演练过:只有"按设计不会发请求"的实现路径。
- 第二个模型只给观点:它不派活、不改记忆、不作数;余额与计费也不自动管。
- 这不是产品:没有安装包、没有多用户、没有客服;出问题看日志,日志看不懂就问我。
七、修订记录
| 日期 | 版本 | 改了什么 | 为什么 |
|---|---|---|---|
| 2026-11-10 | v6.0.0 | 七块收口为一整套;平台明确 Windows 端;压完重载欠账必还;日志可读 | 他要求「发 6.0,作为进入可实用阶段的标注」;编号属例外,编号规则本身未改 |
| 2026-11-10 | v6.0.0 | 版本页移除实现手段与第三方细节,只保留"我们做了什么" | 他定:不能写怎么动官方或开源客户端,只写我们实现了,不给代码 |
v5.9.0 — 空闲期:它自己决定做什么,开关始终在你手里
本页会随勘误就地更新。规范全文以
main为准。
这一版只回答一个问题
没人管它的时候,它在做什么?——以及,谁有权让它停下来?
前几版解决的都是"有事的时候怎么办":到线了压缩、出事了自己修、有新会话了抓取。这一版处理的是没事的时候:队列空了、没人发话、水位也够——它不必再"等下一次输入",可以把自己记下的事拿出来做。
设计思想(为什么给"空闲"单独设计一档)
- 空闲不是"待机",是它自己的时间。 一个只会响应输入的系统,没有输入时就是死的;既然记忆是文件、规则是文件,那么"接下来想做什么"也应该能写进文件,等下一次醒来接着做。
- 谁写的,谁负责。 这些事不是管理员配置的任务,是运行中的那个自己写下的意图(
by: self)。人也可以让它记一条(by: human),但两者记在同一条账上,出处分明。 - 一切是选择,从不指定。 心跳间隔 30 秒只是我们这套的默认值;换一个人设 1 秒、设 1 小时,都只是配置。每个自主动作都能被关掉——
off关整块、drop删单条、needs决定"能不能自己做"、优先级表决定"谁让谁"。没有任何一条是"必须执行"的。 - 自由有边界,而且边界写在代码里,不写在说明书里。 触摸红线的事(删记忆、改内核、换模型、迁移合并、改交接格式、改注入四闸、改成 350K/900s、出网、
pip install、git push)在写意图那一刻就被拒绝,不是等到执行时才拦。 - 不让它饿死,也不让它称霸。 想做的事连续被抢占 3 次、或 24 小时没做成 → 优先级上浮一档;但上浮永远到不了闹钟与人当场指令之上,且每天最多一次。低位不是特权,只是"别永远做不成"。
一条链路(醒来之后,到"它自己找事做")
心跳醒(默认 30 秒,可改)
│
├─ 加载:设定 + 交接包 + 台账
├─ 上报:读全局索引(index_total 合并进简讯)+ ⑦在办 ⑧现在能做 ⑨已自动开工
│
├─ 看任务:在办无未收尾?任务池无未完成?(有活 → 直接干,空闲循环不启动)
│
├─ 有空闲 → 空闲循环(persona_autoloop)
│ ├─ 按优先级挑:自述意图(按条) > 自检 > 检索历史 > 联网(默认关) > 休息
│ ├─ needs=none 且 run 非空 → 执行(可被抢占)
│ ├─ needs=human / external → 只报「要你点头 / 要外部条件」,不执行
│ └─ 全都不满足 → 休息(不执行任何命令,只记一行)
│
└─ 高优先级来了(闹钟 / 人当场指令 / 真任务)
→ 正在跑的低优先级命令 0.5 秒内被 terminate,账本记 preempted_by
→ 压缩 / 镜像 / git / 写 sessions·archive·续接 的命令**不杀**,排队等下次
要实现什么 → 对应机制
| 目标 | 机制 |
|---|---|
| 空闲时有"自己的事"可做 | ops\intents.jsonl 自述意图(只追加);wish 一句话写入 |
| 它想做的事不能越过红线 | wish 时过护栏,命中即 refused 并说明"属需人点头项" |
| 要联网/花钱/要人点头的事不擅自做 | needs: none / human / external 三档,后两档只报不做 |
| 有活时绝不"自行其是" | 只有在办与任务池都空时才 tick |
| 同一件事两套调度打架 | 优先级表 ops\sched-priority.json(数字越小越高),低的一方让位并写日志 |
| 低优先级跑不完 | 租约 + 0.5 秒轮询抢占,preempted_by 留痕;白名单里的命令不杀 |
| 想做的事被长期挤压 | 饥饿保护:连续 3 次被抢占或 24 小时未做成 → 上浮一档,每天最多一次 |
| 管理者随时能停 | off / enabled:false / drop / rm;整块停后其他调度照旧 |
细节(按职责)
- 意图账本:一行一条,
id / at / by / what / why / run / priority / min_gap / needs / status / done_at / result;同 id 后写的为准,历史不删。 - 护栏:匹配的是"动作语义"而不是关键字黑名单——删除/清理记忆、改内核、换模型、迁移合并、改交接格式、改注入四闸、改 350K/900s、出网、装依赖、push,命中即拒并记录拒绝理由。
- 抢占:开跑写
running租约(含 pid 与优先级);轮询到更小数字的pending就自己让位;Windows 先taskkill /T再 kill。被杀的记interrupted,min_gap从开始时间算,避免"跑一半被杀、立刻重跑"的抖动。 - 不杀的清单:
persona_compact、persona_mirror、git、任何写sessions/、archive/、续接/的命令。压缩被杀会留下半截现场,这条写死在代码里。 - 上报:简讯加
⑩ 我留给自己的(最多 3 条)、⑪ 现在要做的、⑫ 被挤压的(被挤掉几次 / 多久没做);只判不执行。
证据(2026-11-10 本机真跑)
$ python persona_sched.py status
job 租约 owner 到期 状态 被谁打断 优先级
compact-check alarm 17:06:26(剩 71s) idle — 1(alarm 1 / daemon 2)
$ python persona_autoloop.py status
意图(ops\intents.jsonl):0 条
(还没有:用 wish "我想做 X" 写一条)
开关:开
$ python persona_autoloop.py --once --dry-run
[dry-run] 休息(没有想做又能做的)
抢占与饥饿保护的沙盒证据(临时库,不碰生产):
- 低优先级
sleep 30在 1–2 秒内被高优先级任务 terminate,账本preempted_by正确、状态interrupted; - 同一沙盒里低优先级跑
persona_mirror.py --check时高优先级来了不杀(标nonpreemptible,等高优先级排队); - 连续被抢占 3 次的意图优先级上浮一次、账本出现
promoted、简讯⑫出现「被挤压」;第四次抢占不再上浮(每天一次上限)。
边界(这一版没做的)
- 没有"无限自由":空闲期随时可被闹钟、人的当场指令、现实法理打断;这一版给的是"被授权的自主",不是权利——AI 还没有独立法理地位,谈权利是空的。
- 不联网、不花钱:
needs=external只报不做;websearch之类留空槽,要开由人填。 - 不做语义级"理解用户喜好":想做的事仍由写意图的那一方决定(它自己或人),没有自动生成"目标"的机制。
- 抢占是进程级的:只能杀子进程;如果某个动作是"在会话里做"的(
run为空的想法),它无法被系统抢占,只能由对话本身让路。 - 跨平台未测:抢占用
taskkill,Windows 之外的实现没验证过(与全局的平台边界一致)。
修订记录
- 2026-11-10:首次发布。编号按人要求取 5.9(v5.8.0 之后的下一个 MINOR)。
- 与 v5.8.0 的关系:v5.8 通的是"机器与机器之间"的管子;这一版通的是"没人发话时"的那段时间。两者共用同一条判据——记忆是文件,模型只是引擎。
v5.8.0 — 记忆网络:几台机器接成一个记忆体
本页会随勘误就地更新。规范全文以
main为准。
这一版只回答一个问题
两台机器,怎样才算「同一个我的记忆」?
不是把"人格"搬过去,是把文件搬过去:设定、会话原文、索引、交接包都是文件。所以这一版做的不是"迁移身份",是给记忆加一根管子——指定一台机器当服务端,其余设备通过虚拟组网连上来,按清单做增量同步。
设计思想(为什么这么设计)
- 记忆是文件,搬家就是搬文件。 不需要任何"人格编解码",也不需要模型参与;同步失败最多是没同步上,不会坏掉记忆本身。
- 真相只有一个。 服务端那份清单就是真相;客户端只按清单拉,不合并、不双写、不反向覆盖。两台机器同时写同一个文件这种问题,不在这版的职责里——那是同目录并发写的问题。
- 落下之前先落盘,落下时原子换。 写盘一律「临时文件 → fsync → os.replace」,任何时刻被杀掉,磁盘上都不会出现写了一半的文件。
- 默认不对外。 不显式指定就只听
127.0.0.1;取不到令牌服务端拒绝启动,没有"默认无鉴权"这一档。 - 只给白名单。
index/、archive/、续接/;..、绝对路径、盘符一律400。
和网盘是两种模式,别混为一谈
网盘模式(persona_cloud_bus) |
节点直连(本版) | |
|---|---|---|
| 解决什么 | 多个程序共用一个目录时怎么写不坏 | 多台机器之间怎么把文件搬过去 |
| 传输谁干 | 外包给网盘 | 自己走 HTTP/1.1(纯标准库) |
| 真相在哪 | 本地那个目录 | 服务端那份清单 |
| 用在哪 | 一台机器 / 一个目录 | 两到几台机器,经 ZeroTier、Tailscale 之类连成同网段 |
| 是否互相替代 | 不替代,可以同时开 | 不替代,可以同时开 |
一句话:网盘管"同一个目录里的并发写",本版管"机器与机器之间的增量"。把两者混成一个"同步",后面就会互相踩。
一条同步从头到尾
客户端 服务端(只有一台)
│ │
├─ GET /health ────────────────────────────────▶ │ 探活,无需令牌,不含路径
│ │
├─ GET /manifest(Bearer 令牌)───────────────▶ │ 白名单目录的 {path,size,sha256,mtime}
│ │
├─ 本地比对 sha256:已一致的跳过 │
│ │
├─ GET /blob?path=…(Bearer)────────────────▶ │ 原始字节,响应头 X-SHA256
│ │
├─ 落 ops/lanbus/ 暂存 → 校验 sha256 → os.replace ─▶ │ 只在白名单目录落地
│ │
└─ (需要回传时)POST /blob?path=…(Bearer)──▶ │ 落盘前再校验一次 X-SHA256,默认 64MB 上限
滚动、重试、跳过都在客户端;服务端只做三件事:报清单、给字节、收字节。服务端不主动推、不删任何文件。
要实现什么 → 对应机制
| 目标 | 机制 |
|---|---|
| 没网盘也能把记忆搬到另一台机器 | 清单 + 按 sha256 增量拉取 |
| 不让人一不小心把库暴露到公网 | --bind 默认 127.0.0.1;启动打印本机 IPv4 并提示「只在虚拟组网网段暴露」 |
| 没有令牌就不存在"裸服务" | 令牌取不到 → 拒绝启动;hmac.compare_digest 比对,不做长度泄露的字符串比较 |
| 不许被路径穿越 | 白名单三目录;..、绝对路径、盘符/冒号 → 400 |
| 杀进程也不会留半个文件 | 临时文件 → fsync → os.replace |
| 日志里能分清是谁 | 所有响应带 X-Node: <主机名> |
细节(按职责读)
- 鉴权:环境变量
PERSONA_LAN_TOKEN优先,其次读本机的令牌文件;服务端启动横幅只打印"令牌来源",不打印令牌内容。 - 权限边界:可写的只有
index/(同步产物)与拉取暂存ops/lanbus/;archive/、续接/只读。 - 拉取的落地顺序:先写暂存 → 校验哈希 → 再原子换进目标目录;校验不过就留在暂存并记一条拒收残片,不动正式文件。
- 可观测:
/health返回节点名、协议版本与可同步文件数(不含完整路径,避免泄露本机目录结构)。
证据(2026-11-10 本机真跑,不是"已实现")
GET /health
→ {"ok":true,"node":"4590-PC","version":1,"root":"lanbus-test","files":2}
GET /manifest(带令牌)
→ 2 个文件:index/a.md(17 字节,sha256 32afc26f…)、续接/b.md(11 字节,sha256 16d0dca6…)
GET /manifest(不带令牌)
→ 401
persona_lanbus.py sync --server http://127.0.0.1:8747 --dry-run
→ 服务端 http://127.0.0.1:8747(节点 4590-PC)
→ 清单 2 个文件(白名单 index/ archive/ 续接/)
→ 比对:需要拉取 0 个,已一致 2 个
→ [dry-run] 只打印计划:不下载、不写 ops\lanbus.json
启动横幅实际打印:令牌来源(内容不打印)、绑定 127.0.0.1:8747(默认安全档,仅本机)、本机 IPv4(127.0.0.1 loopback / 192.168.1.14 网卡)、以及「请只在虚拟组网网段暴露本服务」。
边界(没做的部分)
- 没有托管:没有"中心记忆服务",服务端就是其中一台机器,关机就同步不了。
- 没有第三台中继:两两直连,不做中转。
- 不自动合并冲突:同一个文件两边都改过,本版只按清单判断"谁缺、谁旧",不做语义合并。
- 不做传输加密:加密交给虚拟组网(WireGuard 那些);本服务自身只做鉴权与完整性校验。
- 没有界面:只有
serve/sync/token三个动作;目前也没有接进守护与托盘,不设置就不会自动跑。
修订记录
- 2026-11-10:首次发布——实现入库(此前只存在于作者本机的库根,仓库里只有规范描述)+ 本页说明。
- 编号说明:按人的要求取 5.8(v5.7.0 之后的下一个 MINOR),发布在 v6.0.0 之后,属
RELEASING§三 的例外,已在 CHANGELOG 记明。
v5.7.0 — 简易网络同步
本页会随勘误就地更新。规范全文以
main为准。
这一版只回答一个问题
「好几个程序共用一个网盘目录,怎么写、怎么读,才不会把文件写坏?」
不实现任何传输协议、不绑定任何厂商——只守"文件别被写坏"这一层。
设计思想(守这三条)
- 我们只管纪律,不管传输:同步、加密、断点续传交给现成的网盘/服务器;
- 半截文件是最贵的错:宁可慢一点,也绝不让别的进程读到写了一半的东西;
- 删除不可逆,所以不删:删改一律移进
Archive\,宁可占空间。
步骤:写和读各一条线
写:分片(自己命名,不碰别人的)
→ 网盘可用?
├─ 是 → 写临时名 → rename 成正式名 → 持锁更新总索引
└─ 否 → 追加本地队列(不丢、不阻塞)
读:只读总索引 → 按需去取某一片(不扫全目录)
断网不是异常,是预期内的分支:写不进去就排队,恢复后补交,补交幂等。
要实现什么(目标 → 机制)
| 目标 | 靠什么机制做到 |
|---|---|
| 多个写者不打架 | 各写各的分片,从"抢锁"降级成"零竞争" |
| 观察者永不读到半截 | 先写 .tmp,rename 才是原子替换 |
| 总索引不被写坏 | 单写手 + 文件锁,拿不到锁宁可这次不写 |
| 网盘掉线不挂死 | 降级本地排队,--flush 幂等补交 |
| 误删可回滚 | 删改只移进 Archive\ |
细节(四条纪律 + 为什么)
- 分片写:每个进程写自己的名字(
节点__标题__时间戳),不去 append 别人在写的文件。 - 临时名 + 改名:改名是原子操作,观察者要么看到旧文件要么看到新文件;顺带绕开
"同步客户端在上传期锁住正式文件"的问题——被锁的只是临时名。 - 总索引单写手:聚合视图天生只能一个人改。取锁 → 读全部行 → 删同名旧行 → 追加 → 整体重写。
- 只移不删:删除不可逆,而多进程最常犯的错就是两边同时删。
证据
- 未配置网盘根 → 写入返回
QUEUED,本地队列 +1,进程不挂; - 配好后
--flush→ 队列全量补交、清空; - 3 个进程并发写 → 全部成功,0 个
.conflict、0 个残留.tmp; - 总索引持锁重写:无重复表头、按时间倒序。
边界(没做的)
- 真实网盘环境未验证(本机未配置
PERSONA_CLOUD); Archive\的 GC 独立线程未实现;- 跨时区节点的时间戳会乱序,接第二台机器时要改 UTC。
这只是我们自己的最小实现,仅供参考。 传输、加密、重试交给现成工具即可;
四条纪律可以直接学走,甚至一行代码都不用写。我们的宗旨是:只写最小实现和可能,从不强求别人怎么做。
v5.4.0 — 后台处理:到线自己压,压完把宪法装回来
本页会随勘误就地更新。规范全文以
main为准。
这一版只回答一个问题:上下文快满了,谁先动手?
在这之前,是我盯着水位、想起来的时候自己敲 /compact。这一版之后,是程序自己量、自己敲——
而且它敲的那一下,跟我敲的是同一个东西。
一、设计思想
三条原则,按重要性排。
1. 压归壳,不归我们。
压缩要读完整历史、判断哪段能丢、再写成摘要——只有壳自己干得对,因为它手里才有完整对话。
我们自研一套压缩,等于在没有原文的地方做摘要,压完必然对不上账:摘要里写的,原文可能没有;
原文有的,摘要里丢了。
所以这一版一行压缩算法都没写。我们只是把壳已有的 /compact 替人敲进去。
上一版把压缩标成 PENDING,理由是"壳的压缩接口没有稳定契约"——这个判断没错,
错的是问错了问题:不需要契约,只需要一只手。
2. 到线才动,清之前先落盘。
上下文不是垃圾,是还没归档的现场。清得太早,等于把还有用的东西扔掉;清得太晚,一次请求被拒,整轮白干。
所以线是硬的,而且顺序不可调换:镜像线(300K)永远早于压缩线(350K)——先落盘,再让壳压。
这条线今天重排过一次。原来的梯子是 300K / 600K / 800K,可壳自己的压缩线是
0.35 × 1,048,576 ≈ 367K:人根本活不到 800K,要么中途手动压了,要么先被壳压掉。
于是"到 800K 先镜像再压"变成一条只在纸上成立的规则——判据①挂了一个多月没发生,就是这么来的。
梯子现在按真实到得了的线排:
150K 250K 300K 350K 367K 1M
| | | | | |
注意 备包 镜像→出包 我们敲压 壳自动线 断
(原文先落盘) (冷却900s)
而且"先落盘"不是写在纸上的顺序,是硬闸:镜像/校验失败,ensure_mirror() 直接返回 False,
程序退出、日志记 no-mirror,这一轮就是不压。宁可这轮不压,也不能压完才发现正文没了。
顺带纠正一处只有实现才会暴露的细节:镜像不能复用半小时前那份导出的副本,
差的这几十分钟正是刚聊出来的内容——把它压掉,等于把最该留的丢了。所以每次开火都现导(1–2 秒)。
3. 自动不等于替人决定。
程序可以自己量、自己按,但不许删记忆、不许改库。压缩只作用于当前会话的上下文,
sessions\ 与 archive\ 里的原文一个字都不动。删除永远要人点头——这是红线,不随版本变。
还有一条是今天踩出来的,写进来防下次再踩:闹钟不能挂在会话里。
第一版把这 5 分钟一次的检查登记在会话自己的调度器上,结果压缩一发生、会话被换掉,
闹钟跟着一起消失(事后查 schedule_list 是空的)。能穿越会话的只有系统级计划任务。
3. 压完不算完:宪法要装回来。
压缩是有损的——旧历史变成摘要,摘要里不会有"仲裁梯子""必须人点头的六件"。
上下文一轻,规则也跟着轻,下一轮就容易用"模型默认倾向"顶替"人的当场指令"。
所以这一版还有第二棒:设定重载器(Constitution Reloader)。它和压缩是一对——
压缩负责放下,重载负责留住。
而且它必须是独立进程(他的原话:「5 分钟重读设定和心跳不是一回事,两个是分开的」):
心跳(30 秒)管"系统还活着",重载管"宪法还在不在"。塞进心跳,重载一卡(等焦点、写文件)
心跳就跟着慢——救命的和续命的不许共用一条命。
4. 重载按水位带,不按时间。
第一版我按"5 分钟一次"做,被指出来不对。对的地方在于:记忆散掉跟"带进来多少"相关,跟"过了几分钟"无关。
所以改成水位带:150K 起,每涨 100K 补一次;回落到 150K 以下自动重置。
闲置一小时一次不灌,猛聊二十分钟到带就补。
二、步骤
每 5 分钟(Windows 计划任务 SysPersona-Compact,pythonw,无窗)
│
▼
persona_compact.py check
│
├─ ① 量:gauge 读出一个数「[上下文] 377K」
│ (DSH 增量:ΔcacheRead+Δinput / Δcalls —— 不是那个 56M 的累计缓存)
│
├─ ② 判线:377K ≥ 350K ?
│ └─ 不到 → 记 idle,收工。不打扰任何人,也不写第二条状态
│
├─ ③ 判冷却:距上次开火 < 900 秒 ?
│ └─ 是 → 记 cooldown,收工。一次压缩后水位要过一会儿才落,期间不许连发
│
├─ ④ 先落盘:现导当前会话 → 镜像进 archive\ → 校验 sha256
│ └─ 落不了盘 → 记 no-mirror,**这轮不压**(硬闸,不是提示)
│
├─ ⑤ 不打断:GetLastInputInfo —— 他最近 5 秒还在敲,就等
│
├─ ⑥ 敲:找壳主窗口 → 点输入框 → 剪贴板塞 `/compact`(8 字符)→ Ctrl+V → 回车 → 还原剪贴板
│
├─ ⑦ 壳自己压:旧历史变成摘要,旧上下文不再随请求上送
│
└─ ⑧ 留痕:ops/compact.jsonl(fired / idle / cooldown / no-mirror)+ ops/compact-state.json(last_fire, last_ctx)
│
▼
下一轮 gauge 读到的是回落后的水位(377K → 18K)
一条闭环里只有 ⑥ 在动界面,其余七步全是读文件、导文件、算数、写日志。
这也是它能一天之内接上的原因:那些步骤在前几个版本里都已经跑熟了。
二·五、重载这一棒怎么走
① 水位带到了(150K 起,每 +100K 一次)
│
├─ ② 要不要重载:水位带到了吗?设定 hash 变了吗?被标记了吗?
│ └─ 都没到 → 记 skip,收工(不打扰)
│
├─ ③ 不抢人:他最近 5 秒在敲 → 排队(ops/reloader-queue.json),不注入
│
├─ ④ 不撞自己:会话文件 15 秒不长为空闲 → 才动
│
├─ ⑤ 现抽:从 memory\设定\ 里抽仲裁梯子 / 人点头六件 / 交互铁律 / 每回合四步
│ (现抽,不手抄——手抄的那份一定会过期)
│
├─ ⑥ 注入(≤1600 字):点输入框 → 剪贴板 → Ctrl+V → 回车 → 还原剪贴板
│
└─ ⑦ 记账 + 推进水位带:ops/reloader.jsonl、ops/reloader.json(next_at = 当前 + 100K)
三、要实现什么
| 目标 | 对应机制 | 做到什么算完成 |
|---|---|---|
| 不靠人记得压 | 系统计划任务每 5 分钟一次 | 水位到线后,5 分钟内一定被处理过 |
| 量得准 | 直接读壳的 contextPressure,不自己推 |
托盘、压缩、壳内部判定同一个数(压力口径 147K / 界面口径 86K 分得清) |
| 压得对 | 只敲 /compact,让壳压自己的历史 |
压完水位真的回落(377K → 18K),且对话仍能继续 |
| 不连压 | 冷却 900 秒 + 状态文件 | 刚压过再跑,只记 cooldown,不发第二条 |
| 压前必落盘 | ensure_mirror() 现导 → 镜像 → 校验 sha256,失败即退出 |
镜像失败时一条 /compact 都不会发(不是靠自觉) |
| 不打断人 | GetLastInputInfo,5 秒内有输入就等 |
他打字时不会突然出现被回车发出去的半句 |
| 可复盘 | 两个日志 + 一个状态文件 | 每次开火或跳过,都有时间与当时水位 |
| 穿越会话 | 闹钟挂在系统计划任务上,不挂会话 | 压缩把会话换掉之后,检查仍然每 5 分钟在跑 |
| 压完把宪法装回来 | 独立的设定重载器,压缩成功后直接叫它 | 压缩后一定有一次重载,且不依赖我记不记得 |
| 重载不白灌 | 按水位带(150K 起 +100K),不按时间 | 闲置一小时 0 次灌入;到带才补 |
| 重载不打断人、也不撞自己 | GetLastInputInfo + wait_shell_idle() | 他打字时排队;我生成中不会被吞回车 |
| 重载是真读了 | ||
| eread --ack 记下五份设定的 sha256 | 说"已重载"必须对得上 hash |
四、细节
按职责分六组。每组先说为什么这么做。
1. 量——为什么不再自己算
最早的读法是账本里的 cacheRead(整个会话累计命中量,实测见过 97M)——拿累计量当水位,
人会以为还有 97M 可用,然后下一次请求直接被拒。改成增量 Δ(cacheRead+input)/Δcalls 后好了一些,
但它仍然是一段时间内的平均值:窗口里只要有一次压缩前的大调用,平均值就被拉高。
实测对不上:压缩刚发生时报 18K,过了一会儿报 130K,而壳自己记的是 139K。
所以最后一步是承认这个数不该由我们算。壳在 session_projcache 里每步都在写 contextPressure:
pressureTokens——最近一次调用的真实 prompt 量,壳判"要不要压缩"用的就是它;surfaceTokens——界面上「消息占用」那个数,人眼看到的是它。
现在 gauge 直接读这两个,判定用 pressure、解释用 surface,一个数只有一个来源。
2. 判——为什么要两个门槛,不是一个
只有"到线"一个判断,会让程序在冷却期内反复开火:一次压缩后水位要过一会儿才落,
这期间每 5 分钟量到的还是 350K 以上,于是连发 /compact,把对话搅乱。
所以是"到线 + 冷却已过"两个条件同时成立才动手,冷却 900 秒,开火时间记在状态文件里。
3. 落盘——为什么压之前要先自己复制一份
壳的压缩是有损的:旧历史换成摘要,原文就不在对话里了。摘要是它写的,我们改不了、也验证不了。
所以"清之前先落盘"不是流程讲究,是唯一的补救:落盘的那份才算证据。
这一版把它做成了硬闸——ensure_mirror() 现导当前会话(1–2 秒)→ 镜像进 archive\ →
用 persona_mirror.py 校验 sha256 → 任何一步失败就 return 1,日志记 no-mirror。
反过来说,只要这一版压过,archive 里就一定有一份对得上的原文。
4. 敲——为什么要先看点没点、先看人在不在
壳的输入框在 Electron 的 UIA 树里读不到(只有 13 个元素),所以点击位置是算出来的几何点位:
(left + w/2, bottom - max(70, h/12))。点偏了,粘贴就落在别处,回车等于乱发。
所以流程是:点 → 剪贴板 → Ctrl+V → 回车,最后还原剪贴板(不毁掉他自己复制的东西)。
再加一道:他最近 5 秒内有键鼠输入就等一会——正在打字时把 /compact 敲进去并回车,
等于替他发了半句话。
4. 记——为什么失败也要记
失败不重试(下一轮 5 分钟后自然再试),但必须留痕。最坏的失效不是报错,是"发了但水位不降"——
壳升级改了命令口径时就是这样。这时日志里会留下连续 fired 而水位不降的证据,一眼能查;
静默失效则要等到某次请求被拒才发现。
5. 挂——为什么从会话闹钟改成系统计划任务
见设计思想第 3 条末段。补充一个实现细节:任务用 pythonw 起,配合 persona_win.py 给所有子进程加
CREATE_NO_WINDOW,所以每 5 分钟跑一次也不弹黑窗。
顺带修掉一个只在计划任务里出现的编码坑:gauge 子进程在计划任务上下文里按 GBK 吐中文,
按 utf-8 硬解会得到一串 U+FFFD,于是 [上下文] 对不上、误报「没有输出水位」。
现在改成收字节、两种编码都试,正则也不再依赖中文——只认"方括号后面的第一个数字"。
五、证据
- 真实压了一次,没人按:
ops/compact.jsonl记
{"at":"2026-11-10 09:26:45","ctx":379000,"action":"fired"}—— 379K ≥ 350K,冷却已过,程序自己注入 8 个字符。 - 压完水位真的掉了:同一时刻前后,
persona_gauge.py从377K掉到18K
(下一轮读数 63K,随对话继续增长)。这一轮之后的对话开场即压缩摘要——旧历史确实被换掉了。 - 注入通道有独立留痕:
ops/inject.jsonl同一时间戳多一行{"len":8,"dry":false,"ok":true,"src":"compact"}。 - 防抖可验:
check --dry-run在回落后返回SKIP 水位 63K < 线 350K,不压,退出码 0。 - 计划任务真的在跑:任务
SysPersona-Compact首次执行后LastRunTime有值、退出码 0,
且不是会话级闹钟(schedule_list里没有它)。 - 压前落盘真的会发生:
ensure_mirror()现导 →archive\2026-11-10\电脑会话\续接前-20261110-0938-mirror.md
(231 KB),persona_mirror.py --check回OK … 一致(sha256 与原文相符)。 - 硬闸真的会拦:把 node 指到不存在的路径再跑,
ensure_mirror()返回
(False, '导出异常…')—— 这条链上不会有任何/compact被发出去。 - 重载真的自己跑过一次:ops/reloader.jsonl 记
10:23:27 periodic fired 已注入 870 字,触发原因是水位 306K ≥ 带 150K;
同一份日志里 10:20:29 与 10:22:36 两次都是skip 不需要—— 带内不灌。 - 不撞自己:10:15:38 那次 /compact 注入返回 ok,但水位没降——因为撞上我生成中的一轮,回车被吞。
加 wait_shell_idle() 之后再没出现过;同时reloader那次正确记了queued:他最近 5 秒在操作。 - 水位的"下一带"会自己推进:
reloader --status→水位 311K;下次重载未过带:409K;上次重载 10:23:27。8. 读数与壳同源:壳contextPressure记pressureTokens 139,327 / surfaceTokens 79,875(同一时刻采样),
gauge 输出的就是这两个数;改之前它给的是推出来的130K。
六、边界
- 不自研压缩算法,这一版没有、以后也不打算有——壳手里才有完整历史。
- 重载的正文上限 1600 字:超了会截断并注明"全文在 memory\设定"。超长的那部分确实没进对话。
- 水位带的两个数(150K / 100K)是我定的,理由是"壳压缩线 367K、交接线 300K,150K 大致是注意力开始散的地方"。
它只在persona_constitution_reloader.py一处(CTX_FIRST/CTX_BAND),要改随时改。 - 重载不改任何文件:它只读设定 + 注入一段文字;不删记忆、不动内核、不碰原文。- 摘要实体校验还没做:archive 里有原文,但"摘要里该有的东西是否都还在"没有自动比对。
现在只能靠人(我)在换窗口时读接续包时发现——这不算机制。 /compact是壳的实验特性(协议项compaction_trigger标@experimental,且必须是请求的最后一项输入)。
壳升级改口径,我们就会失效——失效表现是"发了但水位不降",日志留证,不静默。- 托盘还没有「立即压缩」手动项:手动仍然靠人在输入框敲
/compact。 - "到线"依赖 DSH 自己的会话文件:换一个不把上下文写进
~/.dsh的客户端,这条链就得重接。 - 压缩只压上下文,不动
sessions\/archive\里的原文;删除记忆永远要人点头。
修订记录:2026-11-10 首版;同日补「压前必镜像」硬闸与对应证据(步骤、细节、证据、边界已同步)。
v5.6.0 — 记忆网络
本页会随勘误就地更新。规范全文以
main为准。
范围
这一版只回答一个问题:每条记忆,说不说得清是谁记的、从哪来、还能信多久。
现状三处说不清:同一件事被三个模型各记一遍,看起来像三件;命中只给片段,不知道是电脑会话还是网页转述;
摘要覆盖式更新,事后问「当时为什么这么定」答不上来。设计四件:
一、分身 ID:avatar = <agent>-<model>[-<version>],三段全同才算同一个分身。
不取专名只留简称——模型的出厂身份已经有了,别叠加;缺哪段写 unknown
(dsh-unknown 比孤零零的 dsh 诚实),UUID 一律不当分身名。
二、三层溯源,一路回到原文,中间不许"再总结一次":
命中片段(带 avatar / source / 时间)
→ 分索引行:这条属于哪个文件(index\L1.jsonl)
→ 原文:file:line 逐字可读
三、检索只读客观:检索回的是「事实 + 来源 + 时间」,不返回性格、不写性格——
检索改的是"知道什么",改不了"是谁"。
四、性格 = 底座原生,只记录漂移(AVATAR_SPEC §8.1):不建性格面,人格直接用模型出厂神格;
差异不判跑偏、不压制、不写回。漂移只由本分身自己的真实记忆产生(peer: 与检索片段不参与),
由 persona_observe.py 按 persona_key 每天留一行表达快照(句长 / 问句率 / 决定词密度),
diff 只并排给人看,不判正常/跑偏——底座天生带性格,没有零点,也就没有「漂移」(§8.3)。
一句话:允许不同,不许互相污染;要「塑造」人格属于改身份,得人点头。
落地
| 文件 | 做了什么 |
|---|---|
persona_avatar.py |
avatar 唯一入口:认两种头方言(设备/底座 与 来源/模型)、缺段写 unknown、UUID 不当名 |
persona_index_l1.py |
L1.jsonl 每行加 agent/model/avatar;L1.md 加分身列 |
persona_search.py |
结果带分身列;--avatar 过滤;--show-source(文件:行号);--context N(原文前后 N 行) |
persona_master.py |
总索引加「分身分布」 |
impl/launcher/avatar_poc.py |
补 TC-AV7 性格隔离用例:7/7 → 8/8 |
证据
① 三层溯源:10/10 条命中都能在 3 步内读到原文行
② 可重建 :删 index/L1.jsonl → 重建 146 → 146 行
③ 性格隔离:PoC 8/8(AV7:A 变 B 不变 / 检索命中 0 / peer 不入漂移)
分身分布(实测):网页-deepseek-web 92 · dsh-unknown 32 · hermes-deepseek-v4-flash 7 ·
hermes-qwen3-* 12 · unknown 3。
顺手修掉两个真缺陷:--device/--model 对电脑会话一直是空的(导出写「来源/模型」,检索只认「设备/底座」);
分身过滤发生在「取前 60 名之后」,导致 206 条 dsh 命中被误报 0 条——改成先过滤再取候选。
边界
- 历史文件没有元信息头 →
unknown就写unknown,不按内容猜模型(猜出来的来源比没有更坏); - 不做跨设备实时同步:同步是文件层的事,索引只负责"读得对";
- 不为整洁删重复——重复本身是证据(同一件事被两个模型独立记下,说明它重要);
- 性格面不建(2026-11-09 定):人格用底座出厂神格;要「塑造」某分身属于改身份,仍须人点头(5.4 红线);
- 漂移记录只落
ops\drift.jsonl,不进记忆正文——它是观察,不是经历。
修订记录
2026-11-09 首版。同日从「设计成文」推进到「已落地」:persona_avatar.py +
--show-source / --context / --avatar;性格隔离按他的要求折进 AVATAR_SPEC §8(合并章节),不新增节号。
v5.5.0 — 云端任务调度
本页会随勘误就地更新。规范全文以
main为准。
范围
这一版只回答一个问题:活儿是谁记的、谁派的、谁接了,能不能说清。
多端(电脑 / 手机 / 网页 / 别的模型)都能总结出待办。各记各的结果是两个人做同一件事,
或者都以为对方在做——这不是效率问题,是相互抹杀。所以分工写死:
生成归各端,登记与分发归服务端;本地不留私有任务池。
任一端总结 ──► 上交(离线先落 outbox)──► 服务端只做三件事
合并 / 排序 / 匹配
│
派给「最近报到过、能力匹配」的分身
│
ack ──► 执行 ──► done(带证据)
│
超时未接手:expired → 改派(上限 3 次)→ failed + 告警
三条规则写死,不留「我觉得」:
- 合并:标题归一相同,或 2-gram Jaccard ≥ 0.6 → 合成一条,
source_ids全留(丢了就不可回溯); - 排序:
prio1–10,同优先级先看依赖、再看创建时间;服务端只校正不发明; - 匹配:人的指令 > 能力标签 > 最近报到;能力不够的不派。
两个防坑设计:认领用文件锁 + 追加(抢不到就是 exit 1,不靠"应该不会撞");
状态只追加不覆盖(ops/tasks.jsonl 与 dispatch.jsonl 同构),任何一条都能倒查回去。
落地
| 命令 / 接口 | 作用 |
|---|---|
persona_tasks.py submit --offline |
上交;离线落 ops/outbox.jsonl |
flush / merge / pool / show / ack / done |
补交、合并、看池子、追溯、认领、收工 |
serve --port 8787 |
POST /tasks · GET /tasks?agent= · POST /tasks/<id>/ack |
HTTP handler 只有 persona_tasks._Handler 一处,serve() 与自检都从 make_server() 起。
证据
① 不重复:两进程抢同一条 → 退出码 [0, 1]
② 不丢 :outbox 1 条 → flush 后池子 1→2
③ 不卡 :换人次数用满 → failed + 告警
④ 可回溯:show 能追到 source_ids 与提出者
⑤ 合并 :同义标题 → 合并,来源 ['s-1905-07','s-1905-08','s-2']
三接口(随机空闲端口):POST /tasks → 201;GET /tasks?agent= → 200;
POST /tasks/<id>/ack 第一次 200、第二次 409;同标题重提 → 200 且同 id(幂等)。
边界
- 只绑
127.0.0.1,不开公网;跨机靠文件同步或隧道,不靠这个端口; - 托盘「任务池」只读视图还没做(B83):现在看池子要用
pool命令; - 超时改派仍交给已有的
persona_report.py reap,本模块不重复实现一套回收逻辑; - 任务记录进
ops/,不进memory/——任务是活儿,不是经历。
修订记录
2026-11-09 首版。同日从「设计成文」推进到「已落地」:新增 persona_tasks.py;
HTTP handler 抽成一处(第一版自检里复制过一份,已删)。
v5.3.0 — 保活 + 交互代理
本页会随勘误就地更新。规范全文以
main为准。
这一版只回答一个问题
「闹钟响了、它醒了——然后呢?」
v5.3 之前,唤醒只是"到点记录一笔"。醒了之后谁负责、活派给谁、掉线怎么办、无限重试怎么办,
一片空白。这一版把唤醒之后的秩序定下来。
设计思想(守这三条)
- 醒了必须知道做什么:没有
purpose的闹钟一律拒绝登记(退出码 2)。
理由:没有目的的唤醒 = 空转,只是多烧一次上下文。 - 只有"报过到、读过索引"的人才接活:派发有两道门,默认拒绝。
理由:不读索引就接活,等于闭着眼睛干活,交接必然出错。 - 保活只报不拉:进程不在,只把状态写进台账,不替人开第二个窗口。
理由:脚本看不见"没在跑"和"查不了"的区别,照着补开会开出第二份,比不开更糟。
步骤:一条心跳的完整链路
闹钟到点
→ 先摘一份总索引简讯(ops/wake-brief.json,含 index_total)
→ 执行 purpose(环境变量 PERSONA_PURPOSE / PERSONA_BRIEF)
→ 替 agent 报到(read_index=true,记为"已读索引")
→ 派发(两道门:① 最近 600 秒报过到 ② 报到时确实读过索引)
→ 有人接 → 认领;没人接 → 留在池里
→ 超时 → pending 标 expired,改派给还醒着的分身
→ 同一任务改派 3 次仍无人接 → 标 failed + 写 ops/alerts.jsonl(停止重试)
最后那条上限是刻意加的:没有上限的改派是另一种死循环——任务一直在转,看起来永远"处理中"。
要实现什么(目标 → 机制)
| 目标 | 靠什么机制做到 |
|---|---|
| 唤醒可执行、可追溯 | 闹钟带 purpose + 触发记录只追加 ops/wake-fires.jsonl,原记录不被覆盖 |
| 派发不给瞎子 | 两道门(报到时间 / 是否读过索引),不满足直接拒绝 |
| 掉线不丢任务 | 超时回收 reap:pending → expired → 改派给醒着的分身 |
| 无限重试要刹车 | 重试上限 3,超了标 failed 并写告警 |
| 代理必须可控 | 12 个动作、只绑 127.0.0.1、要 X-Persona-Token、逐次审计 |
| 危险命令默认拒 | sys_run 默认全拒,白名单写在 ops/proxy-allow.json |
细节(按职责,不按文件)
- 唤醒(谁该醒、醒了干什么):登记时校验 purpose;到点先摘简讯再执行 purpose;
执行完替 agent 报到,并记下"这次是读过索引的"。 - 派发与回收(活给谁):两个进程分别管"派"和"收"。派只用两道门判断;收只看超时,
超时就把任务放回池子并改派,改派次数记在台账里。台账 append-only,永不覆盖——
这样"这活被谁碰过几次"永远查得到。 - 保活(看不见就别动):只区分"在跑 / 没在跑 / 查不了"三态,后两种都只写状态。
要真的拉起,必须在目标里显式写true——默认不动手。 - 代理(让 AI 能操作本机,但不能乱来):动作表是白名单;网络只绑回环;每个请求带 token;
每一次调用都写审计行,审计里不含 token。 - 自检(拿证据,不拿嘴):
selftest.py用临时库真跑,不碰真实数据。 - 待办备忘(交接包装不下的):把
BACKLOG.md抽成续接\待办备忘.md,交接包只带指针。
证据
python tools/persona/selftest.py→ PASS 36 / FAIL 0(临时库,不碰真数据):- 总索引 3 条来源倒序;
persona_wake无 purpose 登记退出码 2; - 到点触发写入
index_total=3的简讯,并注入PERSONA_PURPOSE; - 派发两道门分别按条件 2 / 3 拒绝;
- 过期记录标 expired 并改派给
rg2,原行未被覆盖; - 超过重试上限标 failed、不再改派,
alerts.jsonl出现dispatch_failed; - 代理 11 项:越界 403 / 未知动作 404 / 未授权
sys_run403 / 加白名单后 200 / 错 token 401,
审计 9 行且不含 token。
- 总索引 3 条来源倒序;
- 实机守护进程:
wakeOK(无到点闹钟)、reapOK(无超时派发)、proxyOK(代理在跑)、
agentOK(在跑);persona_proxy call health返回version=5.3.0;
壳关掉后保活只报状态、不再自动开窗口(验证了"只报不拉"确实生效)。
边界(没做的)
- 唤醒只到"替它报到",还没有把简讯塞进对话——注入通道是 v5.7 之后的事(B71);
- 派发没有跨机器调度,只在单机守护进程内;
- 代理的动作表是固定 12 个,没做通用 RPA(B73)。
v5.2.0 — 托盘状态灯
本页会随勘误就地更新。规范全文以
main为准。
范围
新增 tools/tray/persona_tray.py:Windows 系统托盘,只读守护进程写出的 ops/persona-status.json。
绿=正常、黄=有任务在忙、红=有任务失败或状态超过 3 分钟没更新;双击看各任务最近一次结果;
菜单可打开日志、打开记忆库、退出。只显示,不代办任何任务。
依赖:pystray + Pillow(python -m pip install pystray Pillow)。
证据
- 实机启动 6 秒无异常退出;
- 状态解析覆盖四态:正常 / 忙碌 / 失败 / 状态文件缺失(灰色「读取中」);
- 字段与
persona_daemon写出的persona-status.json(at/tasks.*.ok/busy/msg)逐项对应。
边界
- 不含界面注入,不含记忆写回,不替代守护进程;托盘本身不产生任何数据;
- 仅 Windows;状态文件缺失时只显示灰色,不做重试;
- 同时含一处抓取器修复:无虚拟列表时回退到
document.body,避免整次抓取直接报错(单独提交1efb7ae)。
修订记录
2026-11-09 首版。托盘是「常驻程序看得见」的第一步;后续每个小功能单独切版本。
v5.1.0 — 托盘(控制台)设计
本页会随勘误就地更新。规范全文以
main为准。
范围
docs/TRAY_SPEC.md:托盘(控制台)的定位与路由模型。
- 托盘是控制台,不是智能体:管第三方 agent,不承载对话、不做推理、不跑本地模型;
- 路由:托盘指定一个主节点(主人格或某个分身)→ 主节点按能力与可用性派给分身 → 结果回传 → 分身掉线自动切;
- 功能清单:v5.2.0 状态灯(已发)、v5.3 启停控制、v5.4 任务池视图、v5.5 保活面板;
- 明确否决:托盘内置小模型自己执行任务——路由权归主节点。
证据
本版无代码;判据是把四条边界写死进文档:不跑模型、不写记忆、不直连 API、状态只读 ops/persona-status.json。
边界
不含实现。v5.2.0 的状态灯是本文第一条落地;后续每版只加一个小功能。
修订记录
2026-11-09 补发:先发了 v5.2.0(实现),回头补 v5.1.0(设计)。编号低于 v5.2.0 但提交晚于它,特此明示,避免按时间线误判。