almost-for-ever
zwebzzz/almost-for-ever
Derives the index from your Markdown memory files on every turn instead of keeping a hand-written index table, so renames, splits and deletions never leave a stale entry. Retrieval ranks by library tags, section tags, title tokens and body text, then injects the matching line ranges rather than whole files. Writing is model-proposed and user-confirmed through a sticky note; entries are appended after the target section's format is detected. Ships a structure panel showing each library and section with line ranges and token estimates, a starter template for an empty memory directory, and optional appearance controls.
설치
dsh plugin --profile web add github:zwebzzz/almost-for-everREADME
Almost For Ever
Live memory index for DSH —— 记忆的现场索引、精确检索与便利贴写入。
索引不存盘:每次都从你的记忆文件结构现场派生。写入不靠猜:插件收集候选,你点一下才算数。
包名:almost-for-ever 许可证:MIT 版本:0.4.0
它解决什么
加了 AGENTS.md 之后,大多数人会掉进同一个坑:索引表抄在 AGENTS.md 里,而库名和章节名本来就在记忆文件里——同一份信息存两处,改文件时没人记得改第二处。库改名 → 表里留旧名;拆库 → 表里还是一个;删库 → 表里留着指向虚空的入口。
这个插件把索引换成现场派生:每个会话轮次读一遍记忆目录,有什么文件、文件里有什么 ## 标题,索引就是什么。没有第二份真相可以腐烂。
三层成本
| 层 | 内容 | 成本 | 时机 |
|---|---|---|---|
| L1 目录 | 库名 + 标题 + 章节数 | ~750 tok | 检索时现场算,不常驻 |
| L2 命中 | 命中条目的标题 + 行号 | 110~220 tok | 随用户消息 |
| L3 正文 | 那一条的全文 | 中位 90 tok | 模型用 read 精确读 |
对比「把整张手写索引表常驻 + 命中后整文件读」的做法,典型一次问答从 5,700 + 数千 tok 降到 约 500 tok。
空记忆库怎么办
第一次装上、还没写过任何记忆文件时,插件不会静默装死,而是给一键起手模板:
🗂 还没有找到任何记忆库。
可以一键生成一套起手模板,也可以自己放 markdown 文件进来。
[ 三本起手 ] [ 全部八本 ] [ 英文三本 ]
偏好/项目/环境 再加软件/网络/ Preferences/
命令/清理/排障 Projects/Environment
生成后可以直接改文件,也可以删。不想要就整批删掉。
为什么默认是三本:小白第一次打开就看到一堆空表格,认知负担太重。三本够用,需要再加——所以「全部八本」留在旁边。
绝不覆盖:同名文件已存在就跳过,结果回给用户看「哪些建了、哪些没动」。
读:多维交叉检索
不靠单一关键词。任一维度命中都能排序,说错了别的维度兜住:
| 维度 | 来源 | 权重 |
|---|---|---|
| ① 库主标签 | 文件名 → 标签表(04*=项目、10b*=清理…) | +4 最高 |
| ② 章节标签 | 文件内 <!-- 标签: ... --> | +3 |
| ③ 泛标签 | 父类(项目/游戏/工具/学术/网络) | +0.5 |
| ④ 标题字面 | 中文二元组覆盖率;短查询打 55% 折 | +8×覆盖率 |
| ⑤ 正文兜底 | 只在已有主题分时参与 | +2×覆盖率 |
| ⑥ 状态意图 | 「做到哪了」→ 未结项 ×1.15、已结项 ×0.85 | 乘法微调 |
命中后再精确定位到某一条,给 文件:起行-止行,而不是整个章节——章节可能 4,900 tok,条目中位只有 90。
实测
| 指标 | 结果 |
|---|---|
| 训练集(18 条真实问法) | 前 1 命中 17/18 · 前 3 命中 18/18 |
| 留出验证(10 条调参时没见过的说法) | 前 1 命中 9/10 |
留出验证才是可信数字。训练集 18/18 只说明没被自己写的测试集难住。
三个调出来的坑(都有实测依据)
- 状态不能参与主题分 —— 把「进行中」当 +2 分加,
study-a这种带「进行中」的库会压过真正对口的那个。 - 库主标签必须压过标题字面 —— 问「插件怎么写」时,主标签是「插件开发」的库输给了另一本标题里恰好含「插件市场」的库。
- 短查询的标题匹配不可信 —— 查询只有 1 个词时命中 1 个就是 100%,会把不相干的顶上来。
换个环境也能用吗
能,靠三处「不假设用户跟作者一样」的设计:
| 作者这边 | 换个环境会怎样 | 插件怎么处理 |
|---|---|---|
| 库名固定 13 个 | 用户的库叫 工作日志 / health | 标签表查不到时动态生成(文件名 + H1 + 章节名) |
每库都有 ## 章节 | 用户随手记成一个文件 | 没有 H2 时按条目切节,每条自成一段 |
| 中文查询 | 英文 / 其它语言 | 加英文词干 + 前缀匹配 + 停用词过滤 |
| 下拉写死 16 个目标 | 用户的库一个都选不到 | 下拉目标从实际扫描到的库来 |
两档检索:有高质量人工标签的库走严格档(正文只在已有主题分时参与,防噪音);没标签的库走宽松档(正文可以单独命中,否则彻底查不到东西)。
实测四类「别人」的场景:
| 场景 | 结果 |
|---|---|
| 小白随手记(无章节、无标签) | 3/3 命中 |
| 英文记忆库 + 英文查询 | 3/3 命中 |
| 中文但库名完全不同 | 4/4 命中 |
| 最糟情况(纯散文无结构) | 2/2 命中 |
| 合计 | 12/12 |
而作者自己那份 27 个库没有退化:训练集前 3 命中 18/18,留出验证 9/10。
这组数字来自作者的机器。你的库结构不同,命中率也会不同 —— 想让插件在你的库上跑出可信数字,见下面「开发」一节。
写:模型提议,你确认
📌 值得记下来吗
┌──────────────────────────────────────┐
│ 以后别用 agentrouter 当默认模型了 │
│ 偏好 │
│ 写到 [AI 工具和渠道 ▾] │
│ [记住] [不要] │
└──────────────────────────────────────┘
为什么是「模型判断 + 用户确认」而不是「规则猜」
第一版用正则规则猜「哪句话值得记、该写进哪本书」。实测:
| 规则驱动(v0.2) | 模型驱动(v0.3) | |
|---|---|---|
| 判断写进哪本书 | 30%(留出验证 1/10) | 模型读过记忆库,直接给库名 |
| 判断逻辑代码量 | 404 行启发式 | 0 行 |
| 维护成本 | 加新主题要改词表 | 不用改 |
那 404 行在做的事——主题词表、项目提示、偏好加权、前缀剥离、裸口令识别——本质都在手工复刻模型已经会做的事。
现在的流程:
每 N 轮 / 你说「记一下」
↓
往提示里插一句「回头看看,有值得记的用 memory_propose 提交」
↓
模型自己判断 → 调工具 → 写队列
↓
便利贴弹出 → 你点「记住」→ 追加进记忆库
触发时机:默认每 7 轮自动,或你说「记一下 / 记住 / 记录一下」立刻触发。 不做「会话结束」兜底 —— 模型判断不了会话结束了没有,用户做完事就走了。
提示词里有一句是必须的:「没有值得记的就不用调用,不要为了交差硬凑」。否则模型会为了让工具看起来有用而乱提案。
落盘时的格式守卫
写进目标章节前,插件会先探测那一节的主流格式:
| 目标节 | 行为 |
|---|---|
- 列表 | 补 - 前缀 |
1. 编号 | 接着最大编号往下数 |
| 表格 | 拒绝,回一句「插件不猜列结构」 |
表格拒绝是有意的:列数、对齐、该进哪一列必须人判断。宁可让你手动写一行,也不要往表格里塞格式不对的东西。
队列存在哪
~/.dsh/almost-for-ever/queue.json —— 不放记忆目录。没确认的东西不配叫记忆。
界面:记忆库结构面板
右下角一枚「🗂 记忆库」小圆片,点开是面板:
- 每个库的白话名 + 文件名 + 标签
- 展开看每个章节:行号区间 / 估算 token / 条目数 / 标签
- 顶部概览:最大的几个库各占多少
- 搜索框:按库名 / 章节名筛
用它可以一眼看出「哪个库最胖、该拆了」。
外观:可以按自己的眼睛调
两个浮层(📌 待确认、🗂 记忆库)都能改:
| 能改什么 | 怎么做 |
|---|---|
| 位置 | 按住浮层拖动,松手就记住(位置夹在视口内,拖不丢) |
| 形状 | 胶囊 / 圆角 / 方形 / 圆形(圆形只留图标) |
| 颜色 | 跟随主题,或海蓝 / 雾紫 / 苔绿 / 琥珀 / 玫红 |
| 大小 | 小 / 中 / 大 |
| 复位 | 面板里的「复位到右下角」 |
入口有两个:右键那个圆片,或点开面板后按 ⚙。
存在浏览器 localStorage(键 almost-for-ever:look)—— 这是这台机器的界面偏好,不是记忆内容,所以不进 memory/、不走接口、不同步到别的机器。清掉它会回到默认外观。
为什么默认是「跟随主题 + 胶囊 + 中」:不抢戏。觉得不显眼就挑一个配色 —— 默认特意做得低调,是因为一个常驻在屏幕上的东西太吵会让人想关掉它。
安装
还没发布到 npm,从 GitHub 装:
git clone https://github.com/zwebzzz/almost-for-ever.git
dsh plugin --profile <profile> add <clone 下来的目录绝对路径>
装完关掉 DSH 窗口再打开(桌面端没有「刷新页面」这个操作)。
装完想确认链路通不通:python tools/probe.py —— 把「状态目录 / 接口 / 队列 / 日志」四个环节逐个点亮。
第一次装上、记忆目录是空的也别慌:右下角的 🗂 会提示「还没有找到任何记忆库」,点一下就能生成起手模板(三本 / 八本 / 英文三本)。
配置
- id: almost-for-ever
config:
memoryDir: 'C:\Users\<you>\.dsh\memory' # 记忆库目录
maxHits: 3 # 读取时最多注入几条
budgetChars: 700 # 注入文本的硬预算(字符)
writeEnabled: true # 写入开关
intervalUnit: turns # 触发口径:turns | tokens
intervalValue: 7 # 该口径下的阈值
debug: false # 写 ~/.dsh/almost-for-ever/debug.log
两种触发口径
intervalUnit | intervalValue 含义 | 适合谁 |
|---|---|---|
turns(默认) | 每 N 个用户轮次 | 一般使用;长会话自然会触发多次 |
tokens | 累计输入 token 达到 N | 想按「花了多少钱」而不是「聊了几轮」来控制 |
# 例:每 20k 输入 token 审视一次
intervalUnit: tokens
intervalValue: 20000
给记忆库加标签
在章节标题下加一行 HTML 注释,不改渲染、不占正文:
## game-a / 某个卡牌游戏项目
<!-- 标签: 卡牌, 游戏, 回合制, 进行中 -->
为什么写在文件里,不单独建索引表:删掉这个项目 → 标签跟着消失,不留指向虚空的索引;改名 → 结构变,索引自然变;永远不需要「记得同步」。
⚠️ 别写「用户可能怎么叫」的别名 —— 那是在猜,猜错会让模型产生虚假的信心。标签应该来自正文能证明的事实。
诊断
python tools\probe.py # 状态目录 / HTTP 接口 / 队列 / debug.log
python tools\probe.py --clear # 清空队列
AFE_BASE=http://127.0.0.1:<端口> ... # GUI 换了端口时覆盖
插件的失败模式大多是静默的——收集通道没通、异常被 try/catch 吞掉、接口没注册,界面上都看不出来。这个脚本把四个环节逐个点亮。
开发
test/ ← 谁 clone 下来都能跑(不需要你自己的记忆库)
run.mjs 结构漂移验证(改名 / 删库后索引是否自动跟上)
portability.mjs **通用性**:4 类别人的环境,12 个查询
portability-write.mjs 写入下拉是否跟着用户的库走
portability-diag.mjs 逐条打印「命中了什么、差在哪」
format-detect.mjs 章节格式探测
scaffold.mjs 起手模板生成 + 派生
manifest.mjs 三处模块 id 是否逐字一致
fake-ctx.mjs 共用的假 ctx
test/local/ ← 作者的本地校准(不在仓库里)
eval.mjs 18 条真实问法(训练集)
holdout.mjs 10 条留出问法(**这才是可信数字**)
entry-level.mjs 条目级定位
e2e-format.mjs 三种格式节的落盘行为
smoke-model.mjs 全链路冒烟:注册→计数→提示→工具→队列→落盘
smoke-inject.mjs / smoke-tokens.mjs
node test\portability.mjs # 换环境能不能用(谁都能跑)
node test\manifest.mjs # 改了包名 / 模块 id 之后必跑
想在自己的记忆库上测命中率,用你的真实目录:
# PowerShell
$env:DSH_MEMORY_DIR = "$env:USERPROFILE\.dsh\memory"
node test\run.mjs
不设这个变量时,需要真实库的测试会打印「⏭ 跳过」并正常退出(不是失败)。
本地数据文件(不进仓库)
| 文件 | 内容 | 没有它会怎样 |
|---|---|---|
labels.local.json | 你自己那些库的白话名({"04b-xxx": "某项目"}) | 结构面板 / 下拉里显示成从文件名推出来的默认名 |
设计取舍
| 没做 | 为什么 |
|---|---|
| 自动写入(不经确认) | 记忆库一旦被污染很难发现;确认只需 1 秒 |
| 语义检索(embedding) | 需要额外服务与成本;结构 + 标签已到 9/10 |
| 从正文自动抽标签 | 噪音大:game-b 会因为正文提了一句验证脚本就被贴上「脚本」 |
| 会话结束兜底触发 | 模型判断不了「结束了没有」 |
许可证
MIT。见 LICENSE。