Repository navigation
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 首版;同日补「压前必镜像」硬闸与对应证据(步骤、细节、证据、边界已同步)。