Skip to content

Releases

v6.0.0 — 可用:整套落到一台 Windows 电脑上

Choose a tag to compare

@MirHuang666 MirHuang666 released this 10 Nov 03:22

本页会随勘误就地更新。规范全文以 main 为准。

这一版只回答一个问题:这套东西,现在能真的用起来了吗?

能了。不是"设计写完了",是每天在跑、满了自己处理、处理完自己把规矩装回来、出事自己修的那种能。
所以这一版没有新机制——它把前面一块一块交付的东西串成一台能过日子的机器(而且不再需要手动换窗口:同一个对话里,满了自己处理、处理完自己把规矩装回来),
并第一次把平台写死:Windows 10/11 端可用。

本页只讲我们做了什么、为什么这样做。不讲实现手段,不贴代码,也不涉及任何第三方程序的修改或绕过。


一、设计思想:五条从第一天到现在没变过的话

  1. 人格是数据,不是权重。 换模型是换引擎,换硬件是换车身;身份、人格与记忆才是本体。
  2. 记忆是可迁移的文件,不是任何平台的附属。 原文归档是最后的证据,索引只是"再算一遍"的产物;
    索引可以删了重建,原文不行。
  3. 该醒才醒。 心跳只管"还活着";醒来只做三件:醒 → 报 → 领。叫醒它的一共三档,
    每一档都回答得清"是谁决定的"。
  4. 到线才动,动前先落盘。 提前清等于凭空丢掉还有用的上下文;没落盘就先动手,等于拿记忆赌运气。
    这条在实现里是一道过不去的闸,不是文档上的一句劝告。
  5. 后台干活,不抢前台。 前台在聊,后台在干;先回一句再干活;宁肯排队,也不打断正在说话的人。

还有一条贯穿始终:不依赖云服务,也不修改别人的程序。 采集、索引、检索、交接、水位、调度、托盘全在本机;
断网时唯一停的是"从网上拿新内容"和"请第二个模型说两句",其余照旧。


二、步骤:一条链路,从开机到一整天过去(同一个窗口,不换)

开机 / 登录
  │
  ├─ 采集在后台待命(不需要你打开任何窗口)
  ├─ 常驻程序起来(托盘图标变绿,之后每 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 — 空闲期:它自己决定做什么,开关始终在你手里

Choose a tag to compare

@MirHuang666 MirHuang666 released this 10 Nov 09:08

本页会随勘误就地更新。规范全文以 main 为准。

这一版只回答一个问题

没人管它的时候,它在做什么?——以及,谁有权让它停下来?

前几版解决的都是"有事的时候怎么办":到线了压缩、出事了自己修、有新会话了抓取。这一版处理的是没事的时候:队列空了、没人发话、水位也够——它不必再"等下一次输入",可以把自己记下的事拿出来做。

设计思想(为什么给"空闲"单独设计一档)

  1. 空闲不是"待机",是它自己的时间。 一个只会响应输入的系统,没有输入时就是死的;既然记忆是文件、规则是文件,那么"接下来想做什么"也应该能写进文件,等下一次醒来接着做。
  2. 谁写的,谁负责。 这些事不是管理员配置的任务,是运行中的那个自己写下的意图(by: self)。人也可以让它记一条(by: human),但两者记在同一条账上,出处分明。
  3. 一切是选择,从不指定。 心跳间隔 30 秒只是我们这套的默认值;换一个人设 1 秒、设 1 小时,都只是配置。每个自主动作都能被关掉——off 关整块、drop 删单条、needs 决定"能不能自己做"、优先级表决定"谁让谁"。没有任何一条是"必须执行"的。
  4. 自由有边界,而且边界写在代码里,不写在说明书里。 触摸红线的事(删记忆、改内核、换模型、迁移合并、改交接格式、改注入四闸、改成 350K/900s、出网、pip install、git push)在写意图那一刻就被拒绝,不是等到执行时才拦。
  5. 不让它饿死,也不让它称霸。 想做的事连续被抢占 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 — 记忆网络:几台机器接成一个记忆体

Choose a tag to compare

@MirHuang666 MirHuang666 released this 10 Nov 07:19

本页会随勘误就地更新。规范全文以 main 为准。

这一版只回答一个问题

两台机器,怎样才算「同一个我的记忆」?

不是把"人格"搬过去,是把文件搬过去:设定、会话原文、索引、交接包都是文件。所以这一版做的不是"迁移身份",是给记忆加一根管子——指定一台机器当服务端,其余设备通过虚拟组网连上来,按清单做增量同步。

设计思想(为什么这么设计)

  1. 记忆是文件,搬家就是搬文件。 不需要任何"人格编解码",也不需要模型参与;同步失败最多是没同步上,不会坏掉记忆本身。
  2. 真相只有一个。 服务端那份清单就是真相;客户端只按清单拉,不合并、不双写、不反向覆盖。两台机器同时写同一个文件这种问题,不在这版的职责里——那是同目录并发写的问题。
  3. 落下之前先落盘,落下时原子换。 写盘一律「临时文件 → fsync → os.replace」,任何时刻被杀掉,磁盘上都不会出现写了一半的文件。
  4. 默认不对外。 不显式指定就只听 127.0.0.1;取不到令牌服务端拒绝启动,没有"默认无鉴权"这一档。
  5. 只给白名单。 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 — 简易网络同步

Choose a tag to compare

@MirHuang666 MirHuang666 released this 10 Nov 01:00

本页会随勘误就地更新。规范全文以 main 为准。

这一版只回答一个问题

「好几个程序共用一个网盘目录,怎么写、怎么读,才不会把文件写坏?」

不实现任何传输协议、不绑定任何厂商——只守"文件别被写坏"这一层。

设计思想(守这三条)

  1. 我们只管纪律,不管传输:同步、加密、断点续传交给现成的网盘/服务器;
  2. 半截文件是最贵的错:宁可慢一点,也绝不让别的进程读到写了一半的东西;
  3. 删除不可逆,所以不删:删改一律移进 Archive\,宁可占空间。

步骤:写和读各一条线

写:分片(自己命名,不碰别人的)
      → 网盘可用?
          ├─ 是 → 写临时名 → rename 成正式名 → 持锁更新总索引
          └─ 否 → 追加本地队列(不丢、不阻塞)
读:只读总索引 → 按需去取某一片(不扫全目录)

断网不是异常,是预期内的分支:写不进去就排队,恢复后补交,补交幂等。

要实现什么(目标 → 机制)

目标 靠什么机制做到
多个写者不打架 各写各的分片,从"抢锁"降级成"零竞争"
观察者永不读到半截 先写 .tmp,rename 才是原子替换
总索引不被写坏 单写手 + 文件锁,拿不到锁宁可这次不写
网盘掉线不挂死 降级本地排队,--flush 幂等补交
误删可回滚 删改只移进 Archive\

细节(四条纪律 + 为什么)

  • 分片写:每个进程写自己的名字(节点__标题__时间戳),不去 append 别人在写的文件。
  • 临时名 + 改名:改名是原子操作,观察者要么看到旧文件要么看到新文件;顺带绕开
    "同步客户端在上传期锁住正式文件"的问题——被锁的只是临时名。
  • 总索引单写手:聚合视图天生只能一个人改。取锁 → 读全部行 → 删同名旧行 → 追加 → 整体重写。
  • 只移不删:删除不可逆,而多进程最常犯的错就是两边同时删。

证据

  • 未配置网盘根 → 写入返回 QUEUED,本地队列 +1,进程不挂;
  • 配好后 --flush → 队列全量补交、清空;
  • 3 个进程并发写 → 全部成功,0 个 .conflict、0 个残留 .tmp;
  • 总索引持锁重写:无重复表头、按时间倒序。

边界(没做的)

  • 真实网盘环境未验证(本机未配置 PERSONA_CLOUD);
  • Archive\ 的 GC 独立线程未实现;
  • 跨时区节点的时间戳会乱序,接第二台机器时要改 UTC。

这只是我们自己的最小实现,仅供参考。 传输、加密、重试交给现成工具即可;
四条纪律可以直接学走,甚至一行代码都不用写。我们的宗旨是:只写最小实现和可能,从不强求别人怎么做。

v5.4.0 — 后台处理:到线自己压,压完把宪法装回来

Choose a tag to compare

@MirHuang666 MirHuang666 released this 09 Nov 11:05

本页会随勘误就地更新。规范全文以 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,于是 [上下文] 对不上、误报「没有输出水位」。
现在改成收字节、两种编码都试,正则也不再依赖中文——只认"方括号后面的第一个数字"。

五、证据

  1. 真实压了一次,没人按:ops/compact.jsonl 记
    {"at":"2026-11-10 09:26:45","ctx":379000,"action":"fired"} —— 379K ≥ 350K,冷却已过,程序自己注入 8 个字符。
  2. 压完水位真的掉了:同一时刻前后,persona_gauge.py 从 377K 掉到 18K
    (下一轮读数 63K,随对话继续增长)。这一轮之后的对话开场即压缩摘要——旧历史确实被换掉了。
  3. 注入通道有独立留痕:ops/inject.jsonl 同一时间戳多一行 {"len":8,"dry":false,"ok":true,"src":"compact"}。
  4. 防抖可验:check --dry-run 在回落后返回 SKIP 水位 63K < 线 350K,不压,退出码 0。
  5. 计划任务真的在跑:任务 SysPersona-Compact 首次执行后 LastRunTime 有值、退出码 0,
    且不是会话级闹钟(schedule_list 里没有它)。
  6. 压前落盘真的会发生:ensure_mirror() 现导 → archive\2026-11-10\电脑会话\续接前-20261110-0938-mirror.md
    (231 KB),persona_mirror.py --check 回 OK … 一致(sha256 与原文相符)。
  7. 硬闸真的会拦:把 node 指到不存在的路径再跑,ensure_mirror() 返回
    (False, '导出异常…') —— 这条链上不会有任何 /compact 被发出去。
  8. 重载真的自己跑过一次:ops/reloader.jsonl 记
    10:23:27 periodic fired 已注入 870 字,触发原因是 水位 306K ≥ 带 150K;
    同一份日志里 10:20:29 与 10:22:36 两次都是 skip 不需要 —— 带内不灌。
  9. 不撞自己:10:15:38 那次 /compact 注入返回 ok,但水位没降——因为撞上我生成中的一轮,回车被吞。
    加 wait_shell_idle() 之后再没出现过;同时 reloader 那次正确记了 queued:他最近 5 秒在操作。
  10. 水位的"下一带"会自己推进: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 — 记忆网络

Choose a tag to compare

@MirHuang666 MirHuang666 released this 09 Nov 11:05

本页会随勘误就地更新。规范全文以 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 — 云端任务调度

Choose a tag to compare

@MirHuang666 MirHuang666 released this 09 Nov 11:05

本页会随勘误就地更新。规范全文以 main 为准。

范围

这一版只回答一个问题:活儿是谁记的、谁派的、谁接了,能不能说清。

多端(电脑 / 手机 / 网页 / 别的模型)都能总结出待办。各记各的结果是两个人做同一件事,
或者都以为对方在做——这不是效率问题,是相互抹杀。所以分工写死:
生成归各端,登记与分发归服务端;本地不留私有任务池。

任一端总结 ──► 上交(离线先落 outbox)──► 服务端只做三件事
                                          合并 / 排序 / 匹配
                                              │
                          派给「最近报到过、能力匹配」的分身
                                              │
                                    ack ──► 执行 ──► done(带证据)
                                              │
                          超时未接手:expired → 改派(上限 3 次)→ failed + 告警

三条规则写死,不留「我觉得」:

  • 合并:标题归一相同,或 2-gram Jaccard ≥ 0.6 → 合成一条,source_ids 全留(丢了就不可回溯);
  • 排序:prio 1–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 — 保活 + 交互代理

Choose a tag to compare

@MirHuang666 MirHuang666 released this 09 Nov 09:56

本页会随勘误就地更新。规范全文以 main 为准。

这一版只回答一个问题

「闹钟响了、它醒了——然后呢?」

v5.3 之前,唤醒只是"到点记录一笔"。醒了之后谁负责、活派给谁、掉线怎么办、无限重试怎么办,
一片空白。这一版把唤醒之后的秩序定下来。

设计思想(守这三条)

  1. 醒了必须知道做什么:没有 purpose 的闹钟一律拒绝登记(退出码 2)。
    理由:没有目的的唤醒 = 空转,只是多烧一次上下文。
  2. 只有"报过到、读过索引"的人才接活:派发有两道门,默认拒绝。
    理由:不读索引就接活,等于闭着眼睛干活,交接必然出错。
  3. 保活只报不拉:进程不在,只把状态写进台账,不替人开第二个窗口。
    理由:脚本看不见"没在跑"和"查不了"的区别,照着补开会开出第二份,比不开更糟。

步骤:一条心跳的完整链路

闹钟到点
  → 先摘一份总索引简讯(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_run 403 / 加白名单后 200 / 错 token 401,
      审计 9 行且不含 token。
  • 实机守护进程:wake OK(无到点闹钟)、reap OK(无超时派发)、proxy OK(代理在跑)、
    agent OK(在跑);persona_proxy call health 返回 version=5.3.0;
    壳关掉后保活只报状态、不再自动开窗口(验证了"只报不拉"确实生效)。

边界(没做的)

  • 唤醒只到"替它报到",还没有把简讯塞进对话——注入通道是 v5.7 之后的事(B71);
  • 派发没有跨机器调度,只在单机守护进程内;
  • 代理的动作表是固定 12 个,没做通用 RPA(B73)。

v5.2.0 — 托盘状态灯

Choose a tag to compare

@MirHuang666 MirHuang666 released this 09 Nov 09:09

本页会随勘误就地更新。规范全文以 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 — 托盘(控制台)设计

Choose a tag to compare

@MirHuang666 MirHuang666 released this 09 Nov 09:16

本页会随勘误就地更新。规范全文以 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 但提交晚于它,特此明示,避免按时间线误判。