v0.1.11
更新日志
这是一个稳定性大版本:一次六路并行的全库审计(上下文核心 / 服务层 / 工具层 / 消息层 / Web 前端 / 文档漂移)排出的全部 P1/P2/P3 问题,在本版全部修复——每一处修复都有回归测试钉住,并由独立的只读对抗审计复核。1419 项后端测试 + 239 项前端测试全绿,另用无头浏览器对全新实例做了「引导 → 唤醒 → 对话 → 错误可见」的端到端真机验证。
修复(正确性 / 安全)
- 打断不再毒化上下文:一个回合在工具调用中途被打断(你发新消息抢占了角色的自我工作,或点了停止),过去会在持久上下文里留下「有 tool_calls 却没有对应结果」的残缺记录——此后每个回合都会被严格的服务端以 400 拒绝,直到
/reset。现在中断路径会为每个未应答的工具调用合成一条[interrupted]结果(hermes 同款),上下文永远完整。顺带:因网络 / 流错误被截断的回复,现在标注的是诚实的「连接中断」,而不再伪造成「被操作者打断」。(b0105e4) - 僵尸回合根除:强制接管一个超时未退出的旧回合后,旧回合曾可能「复活」并与新回合同时写同一份上下文/转录。中断标志改为每回合独立,旧回合再也无法吞掉新回合的打断信号。
execute_code死锁解除:工具网关的调度锁过去罩住整个工具执行体,子脚本回调工具时从另一线程撞锁,每次都卡满 300 秒。锁现在只护住审计与守护记录的临界区——子任务(delegate)也因此获得了真正的并行。- 消息网关安全修复:陌生人给机器人发一条私信(即使被拒绝),过去会把角色后续所有主动发言(superchat)的投递目标劫持到陌生人。现在只有通过白名单的发信人才会更新持久投递目标(五个平台 + 宿主 + 独立网关全部修正)。
- 浏览器工具守卫补全:
browser_console的 JS 求值与browser_cdp的导航类调用(Page.navigate/Target.createTarget/Runtime.evaluate等)过去可以绕过browser_navigate的 file:// 与内网/元数据(SSRF)拦截,读取宿主文件。现在三条路径共用同一套 scheme + SSRF + 密钥目录守卫。
修复(行为)
- 切换模型不再遗留旧窗口:
/model、/provider与重配置现在同步重算真实上下文窗口和裁剪缓冲。过去从 1M 窗口的模型切到 128K 会带着过期的 1M 上限逢回合 400;更糟的一种组合会把裁剪目标算成 0,下一条消息静默清空整个活动上下文。两者都不可能再发生。(b0105e4) - 压缩不再复读历史:上下文压缩为恢复而重存的「保护尾」过去会在每次重开聊天与导出里重复出现(实测一次压缩 83 条重复)。重存行现在带
replay标记:恢复用它,显示与导出跳过它。 - 变更型命令与回合互斥:
/compact、/model <id>等会改写共享上下文的命令,过去可以和进行中的回合并发执行(丢消息、请求视图损坏)。现在回合进行中它们干净地拒绝,运行时也会占住回合槽。 - 搜索不再谎报「0 匹配」:
search_files在超时或沙盒拒绝时过去返回干净的 0 结果,把模型带进死胡同;现在如实报错「搜索未完成」。 - 子任务超时按各自起点计:过去按等待顺序累加墙钟,排队未跑的任务会被伪造成「超时」;现在每个子任务从自己启动时刻计时,从未启动的如实报告。
browser_vision在沙盒下可用了:截图过去写往被所有浏览器沙盒拒绝的~/.lunamoth/cache,且给出的绝对路径无法通过 MEDIA 投递。现在落在工作区workspace/screenshots/并给出相对路径。- 消息层整轮加固:QQ 发送改为按
echo关联 OneBot 回执(被拒发送不再假装送达);适配器崩溃即时反映为error状态(不再永远显示「运行中」),微信 / Telegram / Slack 的一次性启动检查移入重试循环;Discord / Slack 出站对 429(尊重 Retry-After)与 5xx 有界重试,长消息中段失败不再无声丢尾;gateway.stop落入每平台开关,不再被无关的配置保存悄悄翻回;关闭自主模式不再切断正在进行的微信回复。 - Web:重进对话不再复读、断线自动重连:离开再进入一个对话过去会把已完成的回合渲染两遍(帧重放与转录恢复叠加);睡眠 / 网络抖动后聊天窗过去永久失联。现在全新进入同步序号、绝不重放;断线按退避永久重试,原位续流,接不上就干净地整页重挂——不会留下无声的空洞。
新增 / 改进(Web)
- 聊天背景与立绘终于有了开关:聊天设置面板新增 聊天背景 / 背景淡化 / 立绘位置 / 立绘浓度——按角色记忆、即调即生效(渲染管线早已就绪,此前只是没有写入侧 UI)。(
27bf74d) - 流式中带着草稿也能停止:停止(■)现在只要在生成中就一直可见,与发送(↑)互不挤占——打了一半的字不再挡住你叫停。
- 市场适配手机:详情页在小屏上撑满整个面板,导入栏吸底并避开安全区;筛选浮层限高可滚动。
- 键盘与读屏可达:看板卡片、卡组书脊、聊天面板行都是真按钮(Enter/Space、可见焦点环、aria 标签,内层按钮不会双触发);弹窗获得完整对话框语义(焦点陷阱、Escape、焦点归还)。
- Superchat 已读态失败开放:
superchat.read写入失败过去让整段历史永远显示未读;现在打开即本地记已读并带重试持久化,水位取气泡的服务器时间戳(客户端时钟偏移不可能「预读」未来消息)。
其余(防御纵深)
请求日志剥离图片字节并按字节裁剪;OpenRouter 目录拉取失败不再被终身缓存(120 秒冷却后重试);/reset 的纪元推导并入写事务,瞬时读失败不可能把角色回卷到远古历史;卡片保存/删除在解析后的真实路径上确认归属;网络开关缺省值漂移修正(旧 env_status.json 不再把角色悄悄断网);MCP 媒体写入路径消毒并确认在媒体目录内。
Changelog
This is a stability release: every P1/P2/P3 finding from a six-track full-codebase audit (context core / server / tools / messaging / web / doc drift) is fixed here — each fix pinned by regression tests and re-checked by an independent adversarial review. 1419 backend + 239 frontend tests green, plus an end-to-end headless-browser verification of a fresh install (onboarding → wake → chat → visible-error).
Fixes (correctness / security)
- Interrupts no longer poison the context: interrupting a turn mid-tool-call (your message superseding self-work, or the stop button) used to leave
tool_callswithout matching results in the durable context — after which every later turn got a 400 from strict endpoints until/reset. The interrupt path now synthesizes an[interrupted]result for every unanswered call (the hermes shape), so the context is always well-formed. Relatedly, a reply cut by a stream/HTTP failure is now marked honestly as a connection error instead of being fabricated as "cut off by the operator". (b0105e4) - Zombie turns eradicated: after a forced takeover of a stuck turn, the old turn could wake up and write the same context/transcript concurrently with the new one. The interrupt flag is now per-turn; a zombie can no longer swallow the new turn's interrupt.
execute_codedeadlock fixed: the tool gateway's dispatch lock used to cover the whole tool body, so a child script calling back into a tool from another thread wedged every such turn for ~300 s. The lock now guards only the audit/guard-record critical section — which also makes delegate workers genuinely parallel.- Messaging security: a stranger DMing the bot (even while refused) used to hijack the delivery target of all future proactive speech. The durable target now moves only after the sender passes the allow-list (all five platforms + host + standalone gateway).
- Browser guard completed:
browser_consoleJS eval and navigate-shapedbrowser_cdpcalls (Page.navigate/Target.createTarget/Runtime.evaluate…) could bypassbrowser_navigate's file:// and private-network/metadata (SSRF) screening and read host files. All paths now share the same scheme + SSRF + secret-dir guard.
Fixes (behavior)
- Model swaps resync the context window:
/model,/providerand reconfigure now recompute the real window and the trim buffer together. Swapping from a 1M-window model to a 128K one used to carry the stale 1M cap (400 every turn); one combination could compute a trim target of zero and silently wipe the entire live context on the next message. Neither can happen now. (b0105e4) - Compaction no longer duplicates history: the protected tail re-persisted for restore used to appear twice in every chat reopen and export (83 duplicated messages measured after one compaction). Re-appended rows are now marked
replay: restore uses them, display and export skip them. - Mutating commands are exclusive with a turn:
/compact,/model <id>and friends used to run concurrently with an in-flight turn (lost messages, corrupted request view). They now refuse cleanly while a turn runs, and hold the turn slot while they execute. - Search never fakes "0 matches": a timed-out or sandbox-refused
search_filesused to return a clean zero, sending the model down the wrong path; it now reports "Search did not complete" honestly. - Delegate timeouts are per-child from each child's own start: the wall-clock used to accumulate in wait order, fabricating "timed out" for tasks that never ran; never-started tasks are now reported as such.
browser_visionworks under the sandbox: screenshots used to target~/.lunamoth/cache(denied by every browser jail) and advertise an undeliverable absolute path; they now land inworkspace/screenshots/with a MEDIA-deliverable relative path.- Messaging hardening across the board: QQ sends correlate OneBot acks by
echo(a rejected send is no longer treated as delivered); a crashed adapter flips status toerrorinstead of a stale "running", and weixin/Telegram/Slack one-shot startup checks moved into retry loops; Discord/Slack outbound retries 429 (honoring Retry-After) and 5xx bounded, and a failed middle part of a long message no longer silently drops the tail;gateway.stopmaterializes per-platform flags so an unrelated config save can't flip it back on; turning autonomy off no longer cuts a live WeChat reply. - Web: no duplicated turns on re-enter, and the chat reconnects: leaving to the board and re-entering used to render completed turns twice (frame replay stacked on transcript restore); after sleep or a network blip an open chat was permanently dead. Fresh entries now sync the sequence and never replay; the socket retries forever with backoff, resumes in place, and falls back to a clean re-attach when the gap can't be bridged — never a silent hole.
Added / improved (web)
- Chat backdrop & sprite finally have controls: the chat settings pane gains 聊天背景 / 背景淡化 / 立绘位置 / 立绘浓度 — per-character, instant apply (the render path existed; only the write-side UI was missing). (
27bf74d) - Stop works with a draft typed: the stop (■) button now stays visible whenever streaming, alongside send (↑) — a half-typed message no longer hides the way to interrupt.
- Market on phones: the detail sheet fills the screen with a sticky, safe-area-aware import bar; the filter popover is height-capped and scrollable.
- Keyboard & screen-reader access: board cards, deck spines and chat-panel rows are real buttons (Enter/Space, visible focus ring, aria labels, no double-fire on inner buttons); modals gain full dialog semantics (focus trap, Escape, focus restore).
- Superchat read-state fails open: a failed
superchat.readused to leave the whole history rendered unread forever; opening a chat now marks read locally with retried persistence, and the watermark uses the bubbles' server timestamps (client clock skew can never pre-read a future superchat).
Everything else (defense in depth)
The request log elides image bytes and trims byte-aware; a failed OpenRouter catalogue fetch is retried after a cooldown instead of being memoized for the process life; /reset's epoch derivation moved into the write transaction so a transient read failure can never rewind a character to ancient history; card save/delete confine on the resolved path; the network-default drift is fixed (an old env_status.json can no longer silently take a character offline); MCP media writes are sanitized and confined.