Skip to content

Releases: westsource/agentctxsync

v2026.10.09.1

Choose a tag to compare

@westsource westsource released this 09 Oct 16:07

v2026.10.09.1 — dsh 适配 DSH Desktop 0.2.0-rc.2:会话格式 v4(原生发布)+ Workspace 分组重建

客户端发布:CLIENT_VERSION 2026.10.06.1 → 2026.10.09.1(mcp/updater.py、server/client_update.py),各端经 /api/client/manifest 自动更新、重启 Agent 后生效。服务端侧只有分发用的版本号变更(client_update.py),无 schema 变更。已部署 236(代码提交 2ef7dbe,3 个文件 LF 归一化预检 / 回读 / 编译全通过,11 项线上核对全 PASS;线上真包解出后就地跑 test_dsh.py 40 项 OK)。

背景:同步下来的会话在新版 DSH Desktop 里打不开

DSH Desktop 0.2.0-rc.2 的会话格式已是 v4(挂载 @deepseek-ai/dsh-session-format-v3-to-v4),而适配器仍宣称「当前世代 = v3」并按 v3 形态发布。dsh 读器对 v3 产物跑 v3→v4 迁移,其目标校验要求 session/title 在 source.kind='user' 时 messageSeqs 必须为空,适配器却给用户标题写了 messageSeqs: [<首个 user/message 的 seq>]。结果是每个带标题的同步会话在桌面打开时都报
「历史加载失败:… session/title messageSeqs must be empty exactly for a user title; source v3 artifact remains unchanged」。

同一版本还修掉两个独立问题:同步进来的会话全部堆在侧栏「未分组」(Workspace 索引只在首启扫描),以及超长路径下的会话被静默丢失。

Fixed(v3 日志过不了 v4 校验 —— 打开即「历史加载失败」)

  • 定位用 dsh 自己的读器(非自证):把桌面 app.asar 里的 dsh/node_modules 解出,用其中的 dsh-session-format-catalog / dsh-session-query 对真实 store 跑 listSessions + readSession —— 410 个日志 6 通过 / 404 失败,错误全是上面的标题规则;把单个日志的标题事件改成 messageSeqs: [] 后同法复跑即通过。改掉标题后又暴露出两处 v4 原生接纳(不再经迁移)的问题:连续 user 行会让上一回合悬空(turn/start does not open the expected turn)、首行即 assistant 的会话缺少受保护的首个 system head(system/message requires a protected first surface head)。
  • 修复(mcp/adapters/dsh.py):
    • _CURRENT_LOG_GENERATION 3 → 4:按 v4 原生形态发布,不再依赖迁移链;旧世代日志即使没有新消息也升级(legacy 判定改用当前世代),前代逐字节冻结、后继写成 session.v4.*。
    • 用户标题事件写 messageSeqs: [](v4 目标规则)。
    • 每轮注入的 system/message 来源由 v3 的 {kind:'plugin', plugin:'@deepseek-ai/dsh-system-prompt'} 改为 v4 要求的 {kind:'system-prompt'}。
    • 回合骨架修正:开新回合前关闭上一个回合(该回合有模型回复 → turn/end.reason.kind='completed',否则 'interrupted');首行即模型回复的会话也先开回合并写入 system head,保证整份日志的第一个表层事件是受保护的 system 消息。

Fixed(同步的会话全堆在「未分组」—— Workspace 索引只在首启扫描)

  • 根因:DSH Desktop 侧栏按 Workspace 分组,成员来自 workspace 存储域(<home>/storages/workspace.json)每条记录的 sessionIds;dsh 只在首次初始化(initialized 尚为 false 时)按会话头 bootstrap 一次,之后永不重扫。适配器此前刻意不写该域(旧版合成行会破坏其不变式),于是所有同步进来的会话都留在「未分组」—— 实测 414 个会话里 383 个本可归属,域里却只有 1 个。
  • 修复(mcp/adapters/dsh.py::_refresh_workspace_index,每次写入后运行):镜像 dsh 自己的 header bootstrap 加性重建归属 —— 按 fs.realpath(cwd) 分组,把本机会话挂到路径相同的记录、缺失目录时新建记录(标题取末段、时间取组内最新会话)。既有记录、标题与展示顺序全部保留(新记录按 dsh create 的约定前置);CJK/超长路径、大小写漂移、junction 都经同一规范。只动已初始化且结构自洽的 unit:文件缺失、外来 unit(name/version 不符)、initialized: false、workspaceIds 与表键不一致或存在 pendingMutation 时一律不碰(交给 dsh);cwd 不存在或没有 cwd(_no-cwd)的会话不分组 —— 与 dsh 自身一致。

Fixed(顺带:长路径会话被静默丢失)

  • _log_file / _session_files / _find_any_log / _log_meta / status / 投影缓存写入此前用裸 Path.iterdir() / read_bytes(),store 根一深(path ≥ MAX_PATH)就抛 OSError 被吞,会话既不读也不写(_session_files 少返、_log_file 返回 None)。现统一走 _lp() 扩展长度形式(新增 _isdir / _scan / _mtime 纯辅助)。实测副本根把某些会话目录顶到 267 字符时,修复前 404/413、修复后 414/414。

测试与验证

  • 端到端(dsh 自己的读器,非自证):把真实 store 的 409 个会话经修复后的适配器全量写入空 store(imported: 409 / new_messages: 26066),再用 dsh-session-query 对写入结果逐个 readSession —— 409/409 全通过(含 1018 事件的长会话、CJK/超长路径会话、_no-cwd 会话)。对真实 store 的副本跑一次升级写(updated: 413、new_messages: 0):前代 v3 逐字节冻结、每会话新增 session.v4.jsonl.zstd 后继,414/414 后继 readSession 通过(修复前 404/413)。桌面实机:重启后打开同步会话正常渲染历史(不再报历史加载失败)。
  • Workspace(dsh 真实域 + 桌面实机):真实 store 副本上先跑一遍:记录 33 → 45、成员 1 → 383,三条不变式(唯一所有者 / 一目录一记录 / 顺序 == 表键)断言通过、二次运行幂等;对真实 store 应用后重启桌面实测:侧栏出现 service / client / diagramon / OpenCode / work / Desktop / D:\ … 等新分组,agentctxsync(69)、中医健康(16)、hermes-sync-foreign(121) 等各归其位,「未分组」只剩 cwd 在本机不存在的会话(本次快照 24 个)。
  • 单测:mcp/tests/test_dsh.py —— 当前世代落盘为 v4、用户标题 messageSeqs 为空、system/message 为 system-prompt 来源、当前世代就地重写、v3 前代升级为 v4 且前代逐字节不变、连续 user 行以 interrupted 收口、模型先行会话以 system head 打头、超 MAX_PATH 的会话目录仍能被 _log_file 读到;WorkspaceIndexTest 7 例。mcp 套件 223 项 OK。
  • 236 部署核对(11/11 PASS):manifest(agent=dsh 与 agent=omp,v=2026.10.06.1)均返回 2026.10.09.1 且 update_available=true;包内 manifest.json 版本一致;包内 mcp/adapters/dsh.py 含 _CURRENT_LOG_GENERATION = 4、{"kind": "system-prompt"}、_refresh_workspace_index、_lp(;包内 mcp/updater.py 为新版本号;包内 dsh.py 与清单 sha256 及提交 2ef7dbe 三者逐字节相符。另把线上 agent=dsh 归档解出,就地对其 mcp/adapters/dsh.py 跑 test_dsh.py —— 40 项 OK。

Docs

  • docs/SUPPORTED_AGENTS.md(DSH 行)与 docs/ARCHITECTURE.md(dsh 小节):当前世代 v3 → v4,补上用户标题 messageSeqs 为空、system-prompt 来源、v3→v4 边与 v4 原生接纳的拒绝规则;workspace 域由「外部不写」改为「按会话头加性重建归属」并写明其不变式与延迟可见(需重启桌面)。

兼容性

  • 已装客户端会在下一次更新检查(每小时一次,或 Agent 重启后 60 秒)自动拉取 2026.10.09.1,重启 Agent 后新逻辑生效;升级后的客户端会把本机既有的 v3 日志在下一次同步时升级为 v4(前代逐字节保留)。
  • 未更新的客户端仍按 v3 发布,在 0.2.0-rc.2 桌面里仍会「历史加载失败」——请更新客户端(或重启 Agent 触发自动更新)。
  • 服务端无 schema/接口变更,旧客户端行为不变。

生产状态:236 已部署(代码提交 2ef7dbe,记录提交 f68b9f6,tag v2026.10.09.1)。

v2026.10.06.1

Choose a tag to compare

@westsource westsource released this 06 Oct 13:42

v2026.10.06.1 — 模型名不再同步:会话的模型选择是设备本地字段

客户端 + 服务端发布:CLIENT_VERSION 2026.10.02.1 → 2026.10.06.1(mcp/updater.py、server/client_update.py,另含 mcp/server.py 与 6 个适配器、server/sync.py),各端经 /api/client/manifest 自动更新、Agent 重启后生效。无 schema 变更(sessions.model 列保留,历史值原样留着,只是不再被同步读写)。服务端 /push /pull 有配套改动:已部署 236(提交 f5be193,10 个文件预检/回读/编译全通过,8 项线上核对全 PASS)。

背景:为什么模型名必须退出同步

model 原先既是 canonical 会话字段、又在服务端按 derived/LWW 处理(无字段级并发保护),而 sync_full 是先拉后推——于是设备上刚切好的模型会在同一轮里被服务端旧值回写覆盖。更糟的是各 agent 的落地并不一致:omp / dsh / reasonix 从不把本机模型上行(omp 读文件时忽略自己的 model_change 条目、dsh 只用它给 assistant 消息盖 source 戳、reasonix 根本没有该字段),而 opencode / hermes / workbuddy / openclaw 是双向同步。同一份"会话"在不同设备上表现为"模型会同步"或"模型不会同步",取决于哪个 agent 打开它。

模型本就是"这台机器这次选用什么"——它是设备级设置,不是共享对话内容。本次把它明确定义为本地字段(local-only),彻底切断两个方向。

Changed

  • 契约:mcp/adapters/base.py 新增 LOCAL_ONLY_SESSION_FIELDS = ("model",),CANONICAL_SESSION_FIELDS 移除 model;通用 SQLiteAdapter._map_cols 跳过该列,1:1 列透传的适配器(hermes)不再把它读进 canonical 载荷。
  • 客户端:新增 _strip_local_only()。推送侧在推送指纹计算之前剥离 model 及消息级副本——切模型既不进服务端、也不再算"内容变更"(不会触发重推);拉取侧在交给适配器写库前剥离——旧服务端/旧对端即使带着它,也盖不掉本机选择。
  • 适配器:opencode 不再读 session.model 与消息 model、不再写 model 列(删除 _model_db;本机已有值原样保留,新建外来会话该列留空);workbuddy 不再读写 model 列(_upsert_session 去掉 model 参数);openclaw 索引不再读写 model,新建 transcript 的必需 model_change 事件写 unknown 占位;dsh 的 assistant source 戳改为 unknown/unknown(删除 _model_parts);omp 不再写 model_change 条目(读时仍容忍真实 omp 文件的该条目);scripts/import_doubao.py 删除 --model。
  • 服务端:push_sync 丢弃载荷中的 model(INSERT/UPDATE 均不写该列),pull_sync 在返回前剥掉。既有行里的历史值保留不动(Web/API 仍可读到,只是不再更新)。
  • 版本:CLIENT_VERSION 2026.10.02.1 → 2026.10.06.1(mcp/updater.py、server/client_update.py)。

测试与验证

  • 客户端 mcp 套件 212 项 OK;服务端 server 套件 292 项 OK / 2 skipped。
  • 新增回归:客户端剥离与指纹隔离、pull 不写回;opencode / workbuddy 本地 model 保全;服务端 push 不写列(INSERT 与 UPDATE 两条路径)与 pull 不下发。
  • 真机冒烟(真实 mcp/server.py + stub HTTP 服务 + hermes 形态 SQLite 库):push 载荷键为 id/agent_type/cwd/started_at/title/message_count/messages/field_meta(无 model),pull 落库新消息后本地 sessions.model 仍是本地值。
  • 236 部署核对 8/8 PASS:manifest(agent=omp&v=2026.10.02.1) → 2026.10.06.1 且 update_available=true、包内 manifest.json 版本一致、包内 mcp/server.py 含剥离代码且 sha256 与清单相符、adapters/base.py 含 LOCAL_ONLY_SESSION_FIELDS、adapters/opencode.py 已无 _model_db。
  • 236 线上功能核对:/pull 不下发 model —— DB 中 631/1012 个会话仍持有 model 值,线上拉一页 15 条(其中 3 条 DB 里确有值),返回载荷中带 model 键的为 0 条;/push 不落 model —— 把一个真实会话按客户端方式重推并注入 model="PROBE-should-not-be-stored",列值仍为原 deepseek-v4.1-flash(209 条消息全部去重、updated=1)。

兼容性

  • 已装客户端会在下一次更新检查(每小时一次,或 Agent 重启后 60 秒)自动拉取新版本,重启 Agent 后新逻辑生效。
  • 升级前的老客户端仍会推 model:服务端已忽略,不会写入;老客户端拉取时服务端已不下发,因此不会再覆盖本机模型。
  • 服务端库里 631 个历史 model 值保持可读(Web/API),仅不再更新。

生产状态:236 已部署(代码提交 f5be193,记录提交 bee8d78,tag v2026.10.06.1)。

v2026.10.02.1

Choose a tag to compare

@westsource westsource released this 02 Oct 08:52

v2026.10.02.1 — 后台同步主备改为持久租约 · 备副本自动接管

纯客户端发布:CLIENT_VERSION 2026.09.21.3 → 2026.10.02.1(mcp/server.py、mcp/updater.py、mcp/auto-sync.py、server/client_update.py),各端经 /api/client/manifest 自动更新、Agent 重启后生效。无 schema 变更,无服务端 API / Web UI 改动。触发场景:同一台机器开多个 Agent 窗口(每个窗口各 spawn 一个 MCP 客户端),实测只有一份在跑后台同步,而角色由"启动抢锁"一次性决定——关掉拥有后台循环的那个窗口后,其余窗口的后台同步会静默停摆。

Fixed(后台同步的角色与并发)

  • 主/备角色改为持久租约,备副本会自我提升:原先角色只在启动时决一次(O_EXCL 锁每周期释放 ⇒ 后启动的副本总能赢下已释放的锁并自认主,副本越多越像多主;而抢锁输家则终身 standby——omp 这类不重拉 MCP 进程的宿主上等于永久停摆)。改为 hermes-sync-<agent>.primary.json 租约(属主 pid + 心跳 + 上次成功同步时间,原子写、每周期刷新):启动时属主仍活着就做备(同时启动仍由 O_EXCL 锁选出唯一主),备每周期复查租约并在属主失联后接管继续跑后台循环。「失联」= 心跳已过一个完整 HERMES_SYNC_INTERVAL 且 pid 探测说死,或心跳超过 HERMES_SYNC_LEASE_STALE_S(默认 INTERVAL+120,兼作 pid 复用兜底);心跳优先,避免误判造出第二个主,代价是失联后最多晚一个周期接管(与同步间隔同量级)。顺带消除"每开一个窗口就重复一次启动全量同步"。
  • Windows 进程判活不再失真:os.kill(pid, 0) 实测三种问题——对 pid 4 抛 SystemError(不是 OSError,原实现接不住,启动任务直接崩、角色卡在 starting);对部分活进程报 WinError 87(被当成死的 ⇒ 会窃取活写者的锁);对刚退出但父进程仍持有句柄的进程报"活"。改用 OpenProcess(SYNCHRONIZE|QUERY_LIMITED_INFORMATION) + 零超时 WaitForSingleObject,不可判定(access denied)按"活"处理;POSIX 分支不变。
  • OpenClaw 常驻同步进程真的持锁了:mcp/auto-sync.py 的文档一直写"与 MCP server 共用同一把锁,绝不并发",但循环体从未取锁(会与 MCP server 的后台周期并发写同一本地库)。现在每轮经 run_once() 取同一把跨进程锁,拿不到就跳过本轮、下一 tick 重试。

Added(省事与可观测)

  • 启动同步新鲜度:上次成功同步的时间记在租约里(同机多窗口共享),距上次成功不足半个同步间隔就跳过启动拉取(另一个窗口刚同步过);手动 sync_full / sync_pull / sync_push 成功后同样刷新。
  • sync_status 增加 instance 段:pid、role、background_sync、lock_holder_pid、lease/lease_owner_pid/lease_owner_alive、log_file、client_version —— "谁在同步、有没有重复"不必再猜。
  • 日志落盘:hermes-sync-<agent>.log(与锁同目录,单代轮转为 .log.1,HERMES_SYNC_LOG_MAX_BYTES 默认 512 KiB)。宿主通常丢弃 MCP stderr(omp 全平台不留存),此前客户端进程崩溃没有留下任何可查线索;stderr 同时改为以 UTF-8 字节写出,避免中文 locale 下 GBK 编码让对方(含 MCP SDK 自身的 stdio 客户端)reader 线程解码失败。
  • 新环境变量:HERMES_SYNC_LEASE_FILE、HERMES_SYNC_LEASE_STALE_S、HERMES_SYNC_STARTUP_DELAY_S、HERMES_SYNC_LOG_FILE、HERMES_SYNC_LOG_MAX_BYTES。

测试与验证

  • 客户端 208 项 OK(194 → 208:新增租约存活判定、备按周期延后与自我提升、启动选举与让位、启动新鲜度、sync_status 实例段、日志落盘与轮转、auto-sync 持锁;删除把旧行为钉死的 standby 测试)。
  • 真机冒烟 13/13(两个真实 mcp/server.py 进程 + 私有锁/租约/日志目录 + server 指向不可达端口,避免触碰本地库):A 选举为主 → B 判为备且不写库 → 杀掉 A 后一个周期内 B 自我提升 → 新副本立刻让位给活属主,sync_status 全程如实回报角色与属主。
  • 236 部署:4 个文件对上一版提交 b8a55ae 逐文件预检通过、上传后回读 sha256 一致、py_compile 通过;重启后 active、journal 无 error、回环 /health=200。线上核对 9/9 PASS:/api/client/manifest?agent=omp&v=2026.09.21.3 返回 2026.10.02.1 + update_available=true、包内 manifest.json 版本一致且 mcp/server.py 的 sha256 与清单相符、包内含租约代码且不再含旧的 "standby 终身" 文案、含新存活探测、mcp/updater.py 为新版本号、mcp/auto-sync.py 持锁。

生产状态:236 已部署(代码提交 b07eb4d,记录提交 86a5ce5,tag v2026.10.02.1)。已装客户端会在下一次更新检查(每小时一次,或 Agent 重启后 60 秒)自动拉取新版本,重启 Agent 后新逻辑生效(正在运行的 MCP server 进程仍持旧代码)。

v2026.09.21.3

Choose a tag to compare

@westsource westsource released this 22 Sep 13:19

v2026.09.21.3 — 文档全面校准 · CI 修复 · 客户端文案纠正

服务端 + 客户端发布:CLIENT_VERSION 2026.09.21.1 → 2026.09.21.3(客户端包有改动:sync_full 的工具描述与占位 Key 前缀),各端经 /api/client/manifest 自动更新、Agent 重启后生效。无 schema 变更。本版汇总自 v2026.09.21.1 以来的两版(2026.09.21.2 / .3)。

Fixed(文档与代码里的不一致)

  • sync_full 的工具描述写反:描述是"先推后拉",实现与契约都是先拉后推(拉取先锚定本机每字段基线,再只推脏字段,避免覆盖同侪的较新元数据)。工具描述与 README(中英)同步更正——这是 Agent 在工具列表里直接看到的文案。
  • 占位 Key 前缀:客户端 HERMES_SYNC_API_KEY 的默认占位串 hsk_placeholder → ws_placeholder;scripts/migrate-local-to-server.py 的提示 hsk_ → ws_(generate_api_key 一直是 ws_)。
  • CI 两个 job 长期为红,本版修好:
    • 客户端 job 未装依赖却要 import mcp/server.py(该模块在模块级 import MCP SDK),dsh 适配器还需要 zstandard → 补 pip install mcp zstandard;
    • 服务端 job 的 import 冒烟测试用 [r.path for r in app.routes],新版 FastAPI/Starlette 把 include 的 router 包成 _IncludedRouter(没有 .path)→ 改走 OpenAPI schema 枚举路径。
    • 按 CI 的两条命令在干净 venv 复跑:客户端 194 项 OK、服务端 285 项 OK(2 skip)。
  • scripts/deploy-remote.py 的上传清单硬编码、含已删除的 feedback.py 且漏掉后来新增的模块 → 改为按 server/*.py 实际文件上传。

Changed(文档按代码事实全面校准)

  • README(中英)新增「What you get / 功能总览」:服务端八个模块面(同步 API、项目 API、Web UI、账号与访问、租户与管理员、配额与套餐、客户端分发、运维)、MCP 客户端全貌(7 个适配器、收敛规则、Primary/Standby 并发、自动更新)、侧车与环境变量指引;快速开始的上传清单补 mcp/(deploy-server.sh 需要它,照抄旧命令会漏)。
  • docs/ARCHITECTURE.md:模块表补齐 ratelimit.py / captcha.py / mailer.py / emailverify.py / jsonbody.py / announcements.py / _run_local.py;schema 由 11 张表补到 16 张(新增 user_verification_tokens、mail_stats、access_stats、access_device、announcement_dismissals);新增「账号、访问与内容治理」决策节(600k 轮 PBKDF2 + 惰性升级、用户名/已验证邮箱登录、账户状态机与"只拦新建工作空间"的门禁、邮箱令牌一次性、自托管验证码、8 个限流 scope 阈值表、发信日预算、访问统计与 src= 真实性、公告、全局搜索、i18n 解析顺序、单 worker + 回环部署约束);路由表补 forgot/reset/security/公告/部署脚本下载;scripts/ 表补齐;组件说明纠正"pgvector"(实际只需标准版自带的 pg_trgm)与侧车命名(按 agent 前缀)。
  • docs/CONFIGURATION.md 重写:服务端 12 个变量(补 PUBLIC_URL / ANNOUNCEMENTS_URL / SMTP_* / MAIL_DAILY_CAP 及缺省行为)、客户端 10 个(补 omp 与锁文件覆盖项)、侧车文件清单与"删掉会怎样"。
  • CONTRIBUTING.md:启动命令 server/server.py → server/main.py;「服务端还没有单测」→ 两套 unittest 的真实命令、数量与 CI 位置。
  • SECURITY.md:PBKDF2 迭代数 100,000 → 600,000(并说明每份哈希自带迭代数、登录时惰性升级);「已知安全设计」补齐租户隔离、限流阈值、自托管验证码、令牌一次性与审计。
  • server/.env.example:补齐可选变量并标注必填项。

Fixed(Web UI)

  • 注册页的密码字段改为纵向排列:/web/register 的「密码 / 确认密码」原本在 grid grid-cols-2 里左右并排(窄卡片里每个仅约 194px),改为各占整行;用户名 / 显示名仍并排(非密码字段)。修正后全站密码字段均为纵向(登录、重置、安全中心修改密码弹窗、首次登录强制改密页、更换邮箱、管理员新建/编辑用户均已核对)。

测试与验证

  • 客户端 194 项、服务端 285 项(按 CI 命令在干净 venv 复跑,全绿);线上(236)65 个上机文件与仓库逐字节一致,/api/client/manifest 返回 2026.09.21.3 + update_available=true,下载包内 manifest.json 版本一致且 mcp/server.py 的 sha256 与清单相符。

生产状态:236 已部署(提交 b8a55ae)—— 预检/回读/py_compile/重启全过,systemctl is-active = active、journal 无 error;server/ 与 mcp/ 共 65 个上机文件对仓库 b8a55ae 零差异;线上 7 项核对(manifest 版本、update_available、包内 sha256、包内文案已为"先拉后推"、占位 Key ws_)全 PASS。v2026.09.21.1 的「按会话暂停/恢复同步」已包含在同一部署中。

v2026.09.21.1

Choose a tag to compare

@westsource westsource released this 21 Sep 11:45

v2026.09.21.1 — 按会话暂停 / 恢复同步(Web 多选 · 服务端冻结 · 客户端补推)

服务端 + 客户端发布:CLIENT_VERSION 2026.09.13.4 → 2026.09.21.1(mcp/updater.py 与 server/client_update.py 同步),各端经 /api/client/manifest 自动更新、Agent 重启后生效。schema 变更:sessions 新增 sync_paused / sync_paused_at 两列,由启动时的 init_db() 幂等 ALTER TABLE ... ADD COLUMN IF NOT EXISTS 创建,无需人工迁移。

Added(Web 端可多选:暂停 / 恢复会话同步)

  • 选定会话暂停 / 恢复同步:工作空间详情页与「全部会话」页每行带勾选框,批量栏一次暂停或恢复所选会话;每行另有单行按钮,会话查看器页也有同一按钮。暂停的会话在列表里带「已暂停同步」徽标。
  • 语义是服务端冻结上行,不是把会话从共享池里摘掉:
    • /push(以及 Web 导入)对这些会话零写入 —— 会话元数据、消息、rev/field_rev、last_synced_at 全部不动。闸门放在 push_sync 最前面,所以配额闸门、消息去重快照与 rev 记账都看不到被暂停的会话:整会话冻结,不是部分合并。
    • /pull 照旧下发冻结版本。否则其它设备会认为"服务端没有这个会话",触发「本地删除不是删除信号」的补回逻辑,或让用户以为会话被删。暂停只断上行。
  • /push 新增响应字段 paused_ids:本次被冻结跳过的会话 id。客户端据此不记录 push 指纹 —— 指纹的含义是"服务端已持有这份内容",记了它,暂停期间的内容在恢复后将永远补不上去(本地无变化 → 指纹命中 → 跳过)。不记指纹 = 每轮重发,恢复后下一轮自动补齐。
  • 路由:POST /web/sync/pause / POST /web/sync/resume,表单字段 sel 可重复,值 "<ws_id>:<session_id>"(会话 id 自身可含冒号,只按第一个冒号切分);只更新调用者自己的 workspace(workspaces.user_id 校验),返回路径 next 限同源。行内已有各自的删除表单,所以每行勾选框用 form="sync-bulk" 关联到行外表单(HTML 不允许嵌套 form),两个提交按钮用 formaction 区分。
  • Web 导入(/web/workspace/{id}/import)跳过暂停会话并在 flash 中报数(与 /push 同规则,不静默丢弃)。

Changed(客户端与文档)

  • mcp/server.py::push_sessions 识别 paused_ids:不计入 processed、不写指纹,日志与返回体给出 paused / paused_ids 计数(多 chunk 合并计数)。
  • 新增词条 sync_pause_* / sync_bulk_* / ws_import_paused(zh-CN + en);_macros.html 新增 sync_paused_chip()。
  • 文档:docs/ARCHITECTURE.md 新增「暂停/恢复同步」决策记录(含混合版本窗口与回归防线)、sessions 表两列、路由表、pull 过滤段说明;docs/server-deployment.md 路由表 / /push 响应字段 / sessions 列表同步;docs/OPERATIONS.md 数据保留章节补条目;README(中英)功能要点同步。

混合版本窗口(实测确认)

  • 未更新的客户端照常自动同步:旧客户端(2026.09.13.4)对新服务端实测 —— 普通会话推送行为一字未变;推送被暂停会话时返回计数 0、无 error,同步循环不中断,同批次其它会话照常入库。
  • 代价:旧客户端会把那次"被跳过"的推送当作成功并写入指纹,于是暂停期间的内容要等该会话本地内容再变化一次、或清一次指纹 sidecar(<mcp目录>/.hermes-sync-field-meta-push-fingerprint.json)才会补上。实测清掉 sidecar 后下一轮即补齐。客户端自动更新后该窗口自然消失。

测试与验证

  • 单元测试:server/tests/test_sync.py(PushTest 新增 sync pause 用例:冻结零写入、同批次其它会话照常落库、响应带 paused_ids、暂停会话不再进入配额闸门)、server/tests/test_sync_pause_ui.py(sel 解析含冒号 id、端点、多选、属主校验、空选择提示、next 同源校验、导入跳过并计数、zh/en 词条齐备)、mcp/tests/test_push_pause.py(不写指纹、下一轮重发、混合版本窗口、多 chunk 合并计数)。全量:server + mcp 477 passed / 2 skipped。
  • 线上(236)20 项检查全 PASS:两列由 init_db() 建出;匿名 POST /web/sync/pause → 307 /web/login;Web 暂停后 /push 返回 paused_ids 且该会话标题/消息数原封不动、/pull 仍下发冻结版本;列表页渲染徽标与批量栏;恢复后同一次推送补齐暂停期间的 2 条消息;/api/client/manifest?agent=hermes&v=2026.09.13.4 返回 version 2026.09.21.1 + update_available=true,下载包内 manifest.json 版本一致、mcp/server.py 的 sha256 与清单相符且含 paused_ids。

生产状态:236 已部署(提交 9531474)—— 11 个文件(6 个 .py + 4 个模板 + mcp/ 客户端 2 个)对上次部署提交 cab5a8a 逐文件 LF 归一化 sha256 预检通过、上传后回读一致、py_compile 通过,重启后 systemctl is-active = active、journal 无 error;check_drift_236.py / check_drift_mcp_236.py 复核 server/ 50 个文件、mcp/ 15 个文件 drift=0。线上 /api/client/manifest 已服务 2026.09.21.1。

仓库说明:v2026.09.19.2 的历史重写(清除部署细节 + 提交身份统一为 noreply)保持不变;本次发布起,新提交与 tag 使用维护者身份 道荣 <westsource@126.com>。

v2026.09.19.2

Choose a tag to compare

@westsource westsource released this 19 Sep 12:48

v2026.09.19.2 — 服务端加固:发信防滥用 · 登录支持已验证邮箱 · 仅回环监听

服务端专用发布:CLIENT_VERSION 保持 2026.09.13.4 不变 —— 客户端包零改动,/api/client/manifest 不触发自动更新,各端无需重启。本版汇总自 v2026.09.13.4 以来的 14 次服务端发布(2026.09.14.1 → 2026.09.19.2)。无破坏性 schema 变更:新增 mail_stats、announcement_dismissals 两张表与 access_device.user_id 一列,全部由启动时的 init_db() 幂等创建。

Added(发信防滥用:收件人侧限流 + 全站日预算 + 验证码)

  • 收件人侧限流(ratelimit.py 新 scope,key 是收件地址/账号而非来源 IP —— 换 IP 不能放大):mail_recipient 1 封/分钟 + 5 封/天(已解析的已验证邮箱)、mail_account 10 封/小时(bind / resend / 安全中心,按 user_id)、mail_address 1 封/10 分钟(调用方自填地址)。此前唯一上限是每 IP 5 封/10 分钟 = 单邮箱 720 封/天/IP。
  • 全站每日发信预算:HERMES_SYNC_MAIL_DAILY_CAP(默认 200),计数落 mail_stats(sent / rejected / failed),DB 计数所以重启不清零;超限拒绝发送并记 rejected(可作滥用告警信号)。动机:发信账号有服务商日额度,被刷爆后所有用户收不到激活/重置邮件,而 /web/forgot 是静默失败,用户只会觉得"没收到"。
  • /web/forgot 挂自托管验证码(复用 captcha,图形为纯矢量线条、答案只存服务端):闸门在任何账号查询之前,失败只回表单页且保留已填标识符,不泄露标识符是否存在。
  • 登录页明确支持"已验证邮箱":标识符可用用户名或已验证邮箱(用户名优先匹配,大小写不敏感;未验证邮箱不可用),标签与提示随邮件功能开关显示;新增 server/tests/test_login.py 8 例覆盖解析规则与"两种标识符失败响应逐字节相同"。

Changed(发信路径与监听面)

  • 验证码改为纯矢量渲染:原先把算式写成 SVG <text> 节点,一个正则即可读出算式;现在只用 <path>(7 段式折线 + 随机旋转/抖动/线宽)与装饰线点,标记里不含任何字符数据。这是成本门槛,不是人性证明 —— 真正的天花板是上面的收件人桶与日预算。
  • 发信移出事件循环:六个发信点全部改走 asyncio.to_thread(对齐 render / sync 的做法)。此前同步 smtplib(timeout=20、每封新建连接)直接在 async 处理器里调用,单进程 uvicorn 下每个发信请求都会卡住登录与同步。
  • 限流计数表满时改为失败关闭:原先超过 20000 个桶就整表 clear(),等于让刷不同 key 的调用方把所有人的预算(含 login 桶)一起清零;现在先淘汰过期条目,仍满则拒绝该请求并打印告警。
  • 服务端只监听 127.0.0.1:8765(原 0.0.0.0):反向代理成为唯一对外入口,明文端口不再出现在任何非回环接口上;request.client.host 只可能来自代理,按 IP 限流与来源审计的前提由"依赖防火墙/安全组"变为"进程自身保证"。自托管部署必须前置反向代理才能从其他机器访问(README / SECURITY / 部署文档已同步)。
  • 侧边栏「官网」入口 + ?landing=1(已登录用户可显式请求落地页),文案「官网」→「官方首页」;「提交反馈」改为 GitHub Issues 外链,删除站内反馈整套死代码。

Fixed

  • REQ src= 不再取 X-Forwarded-For 最左值(那是调用方可控的):改用 uvicorn 解析出的真实对端 IP,原始链另存 xff= 字段。限流一直是按真实 IP 记的(伪造 XFF 绕不过),改的是审计与取证的可信度。
  • 邮箱注册的首次激活必 500:RealDictCursor 上按位置取 RETURNING id,改为按列名取值。
  • 邮件正文改为可点击链接(multipart/alternative,HTML 里锚文本即完整 URL);链接有效期统一为 3 小时并由常量派生(激活链接从 30 分钟 → 12 小时 → 3 小时,重置保持 30 分钟),邮件文案与等待页文案不再各写一份。
  • 落地页「支持的 Agent」计数不再写死(由 PUBLIC_AGENTS 派生,并补齐枚举漏项)。

Added(运营与可见性)

  • 公告横幅(可选,默认关闭):HERMES_SYNC_ANNOUNCEMENTS_URL 指向公开 feed 时,登录用户看到顶部横幅;服务端不出网,由浏览器直取 feed;announcement_dismissals 表 + POST /web/announcement/dismiss 记录已读。
  • 设备访问归属到用户:access_device.user_id 记录 API key 对应的归属,管理页设备明细可按用户/Agent 展开;回填脚本 scripts/backfill-access-device-user.py。

生产状态:236 已部署(提交 cab5a8a)—— server/ 与 mcp/ 逐文件 sha256 与仓库一致、重启后 systemctl is-active = active、/health 200、journal 无 error;线上实测本次加固项(验证码为矢量且无字符数据、错答被拒、正答走统一响应页、REQ src 为真实对端、mail_stats 计数与超预算拒绝)与登录页新文案(中英)。CLIENT_VERSION 保持 2026.09.13.4,线上 /api/client/manifest 仍服务该版本。

仓库说明:同日为清除历史中的部署细节并统一提交身份为 GitHub noreply,对全部历史做过一次重写(含 tag 重指),历史哈希已变化;旧→新映射保存在本地运维手册(不入库)。

v2026.09.13.4

Choose a tag to compare

@westsource westsource released this 13 Sep 11:30

v2026.09.13.4 — 补齐 opencode 的「最后更新时间」落点(13.3 漏判)

客户端发布:CLIENT_VERSION 2026.09.13.3 → 2026.09.13.4(客户端包有改动,各端经 /api/client/manifest 自动更新,Agent 重启后生效)。服务端仅版本常量,无 schema 变更、无数据迁移(13.3 已建的 sessions.last_activity_at 继续用)。

Fixed(opencode 的 session.time_updated 也要用真实活动时间)

  • 漏判:13.3 只改了 workbuddy / omp / openclaw(外加共享的 adapters/base.py),把 opencode 记为"已用 ended_at,不改"——但它与另外三个是同一类落点:session.time_updated 是 opencode 桌面端的排序键,写入用的是 ended_at(缺失才回退 now),而 ended_at 各 agent 语义不一致(13.3 实测 40 条拉取会话:5 条缺失、只有 3 条等于最新消息时间),所以这类会话在 opencode 列表里仍可能显示成"刚刚"或陈旧时间。
  • 修复:mcp/adapters/opencode.py 的 INSERT / UPDATE 两条路径都改用共享助手 session_last_activity()(服务端 last_activity_at → 本次载荷最新消息 → ended_at),保留 >= time_created 下界,now 仅在该会话没有任何可用时间戳时兜底。
  • 7 个注册 adapter 全量复核(hermes/opencode/reasonix/openclaw/workbuddy/omp/dsh,另作者工作区未提交的 chatgpt 只读):改代码 4 个 —— 13.3 的 workbuddy / omp / openclaw + 本次 opencode;hermes 零代码改动(state.db 本就有 last_activity_at 列,进 canonical 后 1:1 映射自动读写);dsh / reasonix 无"最后更新"元数据(dsh 投影缓存只有 createdAt,列表顺序由日志/消息决定);chatgpt 只读上传不写本地。
  • 测试:mcp/tests/test_opencode.py +1(写入与更新路径的 time_updated = 最新消息时间、不早于 time_created)。mcp 套件 196 项、server 套件 196 项通过(仅 3 项改动前既有的环境失败)。文档:ARCHITECTURE 决策记录 2026.09.13.3 的 adapter 映射表与回归防线同步修正(opencode 由"不改"改为"已补齐")。

生产状态:236 已部署 2026.09.13.4(3 文件 sha256 与工作区一致、systemctl restart 后 health 200),线上 /api/client/manifest 服务该版本。

v2026.09.13.3

Choose a tag to compare

@westsource westsource released this 13 Sep 11:18

v2026.09.13.3 — 会话最后活动时间由服务端按消息派生

客户端发布:CLIENT_VERSION 2026.09.13.2 → 2026.09.13.3(客户端包有改动,各端经 /api/client/manifest 自动更新,Agent 重启后生效)。服务端有 schema 变更:新增 sessions.last_activity_at(可空,启动时幂等 ALTER 建列),存量行由 scripts/backfill-last-activity.py 一次性回填(生产已执行 691 行)。

Fixed(拉取到本机的会话"最后更新时间"是同步时刻,不是最新消息时间)

  • 现象:从远端同步下来的会话在本地一律显示成"刚刚"。WorkBuddy 列表按 COALESCE(updated_at, created_at) 排序 → 一次同步把被触碰的会话全部塌到同一个时间戳并顶到最前(实测本机 workbuddy.db:6 行最近被触碰的会话 updated_at 全等于同步时刻,而其最新消息分别在 08-21 / 08-29 / 09-03);Hermes 桌面端的 last_activity_at 对同步创建的会话是 NULL;omp 的 title slot updatedAt、OpenClaw 索引的 updatedAt/lastInteractionAt/lastActivityAt 都恒写 now。
  • 根因:last_activity_at 不在 canonical 模型里,各 adapter 只能拿 ended_at,而它各 agent 语义不一致(2026-09-13 实测 40 条拉取会话:5 条缺失、只有 3 条等于最新消息时间);workbuddy 的写入路径还把值 max(..., now) 钳到同步时刻(来自最初的 adapter 提交,无注释、无测试守住)。
  • 修复(服务端派生 + 客户端消费):sessions.last_activity_at = 最新一条可见消息的 timestamp,由 server/sync.py::push 从本次载荷的消息派生(客户端同名字段被覆盖;无消息的元数据型 push 透传客户端值,不清空)。mcp/adapters/base.py 新增共享助手 session_last_activity()(服务端值 → 最新消息 → ended_at → now),落地点:workbuddy updated_at+last_activity_at(去掉 now 钳制,仅保留 ≥ created_at 下界)、omp title slot updatedAt、openclaw 索引三字段(并读回 lastActivityAt)、hermes 零代码改动(state.db 本就有该列,进 canonical 后 1:1 映射)。opencode(已用 ended_at)/dsh/reasonix(无该元数据)/chatgpt(只读)不改。
  • 存量回填:scripts/backfill-last-activity.py(dry-run 默认 / --apply / 幂等 / 只看可见消息)。
  • 验证:本机真实 store 重新落地一条 08-21 的会话 → updated_at/last_activity_at = 08-21 22:44:27(其最新消息时间),同步发生在 09-13 19:01;生产回填 691 行、复跑 0 改动;线上 /pull 抽样 25 条:24 条与 MAX(m.timestamp) 完全相等、0 条不符、1 条无可见消息(NULL,按设计)。mcp 套件 195 项、server 套件 196 项通过(仅 3 项改动前既有的环境失败)。

生产状态:236 已部署 2026.09.13.3(9 文件 sha256 与工作区一致、systemctl restart 后 health 200),线上 /api/client/manifest 服务该版本,发货 zip 含新代码。决策记录:ARCHITECTURE「会话最后活动时间(last_activity_at)」;运维说明:OPERATIONS「Session Last Activity」。

v2026.09.13.2

Choose a tag to compare

@westsource westsource released this 13 Sep 06:59

v2026.09.13.2 — workbuddy 项目清单接入共享项目池 + 根目录不入池

客户端发布:CLIENT_VERSION 2026.09.13.1 → 2026.09.13.2(客户端包有改动,各端经 /api/client/manifest 自动更新,Agent 重启后生效)。服务端:仅 client_update.py 的版本常量,无 schema 变更、无数据迁移。

Added(workbuddy 项目清单接入共享项目池:本机项目列表与服务端项目卡片对齐)

  • 背景:项目是工作空间级共享池(/api/projects/pull 返回全部可见项目,与 agent 无关),此前只有 hermes 在推。WorkBuddy 桌面端的项目清单(workbuddy.db 的 workspaces 表:path + last_opened_at)完全没有上行——服务端 10 个项目全是 hermes 的;网站项目卡片按 cwd 前缀关联会话,因此本机 286 条 WorkBuddy 会话里 191 条落在项目之外。
  • 实现(mcp/adapters/workbuddy.py,supports_projects = True):read_projects 每条 workspace 路径一个项目——服务端已知路径原样使用服务端的 id/slug/name/folders(身份存于 .workbuddy-sync-projects.json 侧车;改用目录名派生会被判"本地脏"并每轮覆盖对端项目名),未知路径铸确定性 wb_<sha1(_path_key)> + 整路径压平的 slug(逐路径唯一,不会触发服务端同名合并)。write_projects 按本次 pull 全量重建侧车,并为每个项目路径补 workspaces 行(建目录);已有行只保留、不再定日期,本地行永不删除。
  • 能力位:新增 supports_projects(默认 False)——无本地项目库的适配器在周期同步里跳过项目阶段,工具面返回 Agent X has no local project store …,此前每轮记一条 AttributeError 日志。
  • 线上验证:pull 落地 8 条 workspace、采纳 9 个服务端项目身份;push 服务端 10 → 13 且既有 10 个项目零字段改动(imported:3, updated:9, merged:0);二次 push 幂等。

Added(根目录不入共享项目池:识别根形状路径并排除,退役已上行的根卡)

  • 背景:D:\、C:\Users\rong 这类"根"条目会让项目卡变成兜底桶(实测 rong 吞 99 条、D: 吞 27 条 = 110/286,其中 62 条是 ~/hermes-sync-foreign 合成目录);而服务端 project_folders 只并集、无删除 API,项目也没有 Web 隐藏/删除入口 ⇒ 一旦上行即永久。故"各 agent 拉到本机后把本机 home 并进根项目"的方案被否决(四条理由与被保留的"服务端可回退 alias"备选见 ARCHITECTURE 决策记录 2026.09.13.2)。
  • 实现:base.is_root_project_path / strip_root_project_paths(盘符/filesystem 根、/root、/home·C:/Users 及其子项);push/pull 边界统一过滤——纯根项目整条不上行/不落地,根 primary_path 只置 None、绝不改写(它是 per-field LWW 字段,改写会与对端逐周期互推覆盖);workbuddy 适配器另清理身份侧车。本地路径全保留(D:\ 仍是 WorkBuddy 的 workspace,hermes projects.db 不动)。
  • 退役:scripts/hide-root-projects.py(dry-run 默认 / --apply / --undo)置 hidden=1——/api/projects/pull 与 Web 项目列表都过滤 hidden,且 push 的 UPDATE 分支从不写 hidden ⇒ 客户端推不活(已用旧客户端语义复推实测)。
  • 实测影响:服务端项目卡 13 → 11;本机参与同步的 workspace 键 15 → 13;本机会话归组 207/286 → 97/286(诚实结论:项目同步对本机会话分组的净收益≈0,真实收益是"项目清单对齐 + 其他机器/agent 的会话能归入这些项目")。

生产状态:236 已部署 2026.09.13.2(6 文件 sha256 与工作区一致、systemctl restart 后 health 200),线上 /api/client/manifest 实测 served 2026.09.13.2,发货 zip 逐文件 sha256 校验通过;两张根卡已在生产退役(hidden=1,dry-run 幂等、复推不复活)。测试:mcp 套件 185 项(仅 3 项改动前既有的环境失败:zstandard 未装 ×2、SyncLockAndRoleTest ×1)。

v2026.09.12.3

Choose a tag to compare

@westsource westsource released this 12 Sep 12:48

v2026.09.12.3 — dsh 会话世代选择与写入目标修正

客户端发布:CLIENT_VERSION 2026.09.12.2 → 2026.09.12.3(客户端包有改动,各端经 /api/client/manifest 自动更新)。服务端无 schema 变更。

Fixed(dsh 世代选择方向反了 —— 迁移过的会话读到冻结的旧日志)

  • 根因:dsh 的会话日志有世代——session.jsonl[.zstd] = v0,session.vN.jsonl[.zstd] = vN(当前 v3)。dsh-session-persistence-jsonl 明确规定:运行期读与写都取数值最高的世代;写入发布后继代并保持源文件逐字节不变(迁移过的 v0 从此冻结,只有 v3 继续增长),且保留的前代不提供降级读取。
  • 旧 _log_file 先试 session.jsonl / session.jsonl.zstd、找不到才回退 vN —— 方向正好相反:对迁移过的会话会永久读到冻结的 v0(丢失后续轮次与标题),写入则落进 v0 —— 一个 dsh 永不读取的文件(写入实际无效,还在存储里留下误导性副本)。
  • 修复:_log_file 镜像 dsh 的规则(按 (世代, 压缩优先, mtime) 取最高世代);_write_log 保持会话既有的世代与编码(_log_filename(gen, zstd)),不再在 vN 会话旁写 v0;并原样保留既有 header(isSeeded / agentPreset 等世代专有字段据此存活),物理 header 的 version 恒等于文件名世代;对合成的 vN header 兜底补 isSeeded(dsh 的 v2→v3 header 校验要求该字段)。读取侧无需改动:_event_message 本就同时支持 v3 行形态(data.{role,content} 与 data.message.{role,content})。
  • 验证用 dsh 自己的编解码器(非自证):在真实 v3 会话上写入一次后,@deepseek-ai/dsh-session-format 的 readHeader 返回 current(stored 3 → target 3),encodeCurrentEvent 逐行接受全部 9 个事件;写入后目录中只有 v3 文件被改写(冻结的 v0 逐字节不变)。命名侧与 dsh 的 parseSessionFormatLogFilename 逐例一致(去掉 .zstd 后 0/1/3/12 全等,垃圾名同样拒绝)。
  • 测试:mcp/tests/test_dsh.py::LogGenerationTest 6 例;mcp 套件 146 项、server 套件 189 项通过。