Releases: OpenChara/OpenCharaAgent
Release list
v0.2.1
v0.2.1
The first release as a settled OpenCharaAgent: new positioning, a friendlier README, a WeChat-style mobile home, and a cleaner chat model.
Positioning
- New tagline: Create with your characters / 和你的角色一起创作. The "original character" claim is retired; the market imports any card.
- Both READMEs rewritten in a plain register with real UI screenshots. New banner (33KB WebP).
- The project site lives at https://agent.lunamoth.ai (the apex redirects).
Chat
speakrenders as exactly one highlighted bubble, identical live and after a reload. The tool chip and the raw{"ok": true}result chip that appeared after reloads are gone.- The superchat read/unread feature is removed.
- The board and list previews show the last conversation message (yours, the chara's reply, or a speak, whichever is newest). The card's greeting no longer masquerades as activity.
- On phone widths the characters home is a conversation list: avatar, autonomy dot, name, relative time, one-line preview. Tap to open the chat.
World books
- World info now works like memory. Constant entries stay in the cached prefix as the world's fixed overview; keyword entries are recalled per turn while their keyword is in the recent scan window and leave when it scrolls out. The 4-turn sticky tail-off is removed and the recall budget drops from 25% to 10% of the window.
recall_entriesis the single retrieval seam a future GM model will take over.
Audited
An independent audit reviewed everything in this release before tagging. It caught one real defect the test suite missed, fixed here: replies from reasoning models are stored differently at rest and never reached the board preview; they do now.
Breaking
- The v0.2.0 rename compatibility layer is removed: no
lunamothCLI alias, noLUNAMOTH_*env fallback, no automatic~/.lunamothdata migration, andextensions.lunamothcard namespaces are no longer read. Upgrading from 0.1.x means a fresh install (curl -fsSL https://raw.githubusercontent.com/OpenChara/OpenCharaAgent/main/install.sh | bash) and re-creating charas, or migrating data by hand first via a 0.2.0 stopover.
v0.2.0
v0.2.0 — LunaMoth is now OpenCharaAgent
The project has a new name and a new home: OpenChara/OpenCharaAgent. Same runtime, same philosophy — the name now says what it is, and the moth stays as our sigil.
What changed
- Repo:
Lunamos/LunaMoth→OpenChara/OpenCharaAgent. GitHub redirects every old URL (clones, the installer, the in-app updater all keep working). - Command: the CLI is now
chara(chara,chara tui,chara desktop --daemon, …). The oldlunamothcommand remains as a deprecated alias for one release cycle, so existing scripts and systemd units keep working. - Data dir:
~/.lunamoth→~/.chara. Migration is automatic on first run (a symlink is left at the old path so anything not yet restarted keeps resolving). ExplicitLUNAMOTH_HOME-style env pins are honoured as-is. - Env vars:
LUNAMOTH_*→CHARA_*. Every legacyLUNAMOTH_*variable is still read as a fallback — no deployment change required. - Package: the wheel/distribution is
opencharaagent; the import package ischara.
Upgrading an existing install
The in-app update button on an old install will fail on this release (the distribution name changed under it) and will hand you the manual command — which is simply the installer again:
curl -fsSL https://raw.githubusercontent.com/OpenChara/OpenCharaAgent/main/install.sh | bashThat migrates your data dir, retires the old tool registration, and installs chara. Your charas, cards, keys and sessions all carry over untouched. Restart any running daemon afterwards (chara daemon stop && chara desktop --daemon).
Unchanged
- Card format (SillyTavern V2/V3) and all existing cards — including each living chara's frozen card. The
extensions.lunamothcard namespace is still read everywhere it was before. - The protocol, the gateway surface, your session data.
v0.1.13
更新日志
第三轮加固:审计触角伸到最后一块未覆盖的层——命令行前端、自更新器、安装脚本、部署与 CI——外加一轮双语 README 校对和真实浏览器的交互走查。所有发现(包括对抗复核对修复本身提出的 5 个反例)全部修复。1452 项后端 + 243 项前端测试全绿。
修复(供应链——发布管线的校验承诺现在端到端成立)
install.sh在默认公开安装路径上其实什么都没校验:SHA256SUMS 校验块被一个只在私有下载路径才赋值的变量短路——CI 精心生成的校验清单从未被查阅,curl | bash装进来的 wheel 完全未经验证。现在无论哪条路径都是:下载到本地 → 对发布清单验证 sha256 → 从验证过的本地文件安装;不匹配大声拒绝;清单缺失或机器上没有 sha 工具会得到诚实的 NOTE,绝不静默跳过;临时文件在所有退出路径上由 trap 清理。(53b9180)lunamoth update同样不校验:自更新过去直接把 wheel URL 交给安装器。现在同样下载 → 验证 → 本地安装,校验不匹配或清单无哈希一律拒绝;下载与安装共享同一个墙钟预算,不会撑爆桌面端的 RPC 上限。
修复(命令行 / 守护进程)
- wheel 渠道下首次运行向导的角色菜单是空的:它扫描的是源码仓库的 cards/ 目录(只在 dev checkout 存在),而不是打进 wheel 的内置卡。所有八张内置角色卡现在在向导里可选。
- 守护进程 PID 卫生:重启电脑后残留的
daemon.pid可能让start-all静默跳过一个角色,或让stop向一个复用了该 PID 的无关进程组发信号。现在:存活检查附带身份验证(每个角色的守护进程 argv 携带--session标记,兄弟角色互不误认,升级前的无标记进程仍然识账);启动前以 O_EXCL 原子占位杜绝双重拉起;stop不再误删正在启动中的占位。
修复(文档,中英同步)
- 消息平台表修正为真实的五个平台(WeChat iLink / QQ / Telegram / Discord / Slack——移除了早已砍掉的 WeChatPadPro 行);首启流程按现状重写(选语言 + 描述角色,模型在设置里配,预设列表与代码一致);已删除的"愿望"概念替换为现行的 理想 → 任务 模型;卡片市场与 SillyTavern 导入从"未来计划"移入已上线;补充了生图、视觉资产管线、个人网站模块、聊天背景/立绘偏好的说明。
修复(UI)
- 首次运行的欢迎层现在响应 Escape 关闭,与其他所有弹层一致。交互走查确认:建卡流程、脏改动守卫、生成失败的可见错误路径全部行为正确。
Changelog
The third hardening round: the audit reached the last uncovered layer — the CLI front end, the self-updater, the install script, deploy and CI — plus a bilingual README accuracy pass and an interactive real-browser walk. Every finding (including the five counter-findings the adversarial review raised against the fixes themselves) is resolved. 1452 backend + 243 frontend tests green.
Fixes (supply chain — the release pipeline's verification promise now holds end to end)
install.shverified NOTHING on the default public path: the SHA256SUMS block was short-circuited by a variable only set on the authenticated download path — the manifest CI carefully publishes was never consulted, and acurl | bashinstall shipped an unverified wheel. Every path now downloads locally → verifies sha256 against the release manifest → installs from the verified file; a mismatch refuses loudly; a missing manifest or missing sha tool gets an honest NOTE, never a silent skip; temp files are trap-cleaned on every exit. (53b9180)lunamoth updatedidn't verify either: self-update used to hand the wheel URL straight to the installer. It now downloads → verifies → installs locally, refuses on a mismatch or a hashless manifest, and shares one wall-clock budget across download + install so the desktop RPC ceiling holds.
Fixes (CLI / daemon)
- The first-run wizard's character menu was empty on the wheel channel: it scanned the repo-root cards/ directory (which only exists in a dev checkout) instead of the bundled cards baked into the wheel. All eight bundled characters are now selectable.
- Daemon PID hygiene: a stale
daemon.pidsurviving a reboot could makestart-allsilently skip a character, orstopsignal an unrelated process group that reused the PID. Liveness now includes an identity check (each daemon's argv carries a per-session--sessionmarker — sibling characters can't be mistaken for each other; pre-upgrade markerless daemons still pass); starts claim the pid file atomically (O_EXCL) before spawning;stopno longer deletes an in-flight start claim.
Fixes (docs, EN + zh in lockstep)
- The messaging table now shows the real five platforms (WeChat iLink / QQ / Telegram / Discord / Slack — the long-dropped WeChatPadPro row is gone); the first-run flow matches reality (language + describe-a-character; the model is configured in Settings, with the preset list matching the code); the removed "wishes" concept is replaced by the current Aspiration → task model; the card Market and SillyTavern import moved from "future roadmap" to shipped; image generation, the visuals pipeline, the personal-website module, and chat backdrop/sprite prefs are documented.
Fixes (UI)
- The first-run welcome now closes on Escape, like every other layer. The interactive walk confirmed the create flow, the dirty-change guard, and the visible generate-failure path all behave correctly.
v0.1.12
更新日志
紧随 v0.1.11 的第二轮全面加固:换一批新眼睛对全库再审一遍(上下文核心 / 服务层 / 工具层 / Web / 提示词与 harness 设计),加一轮真实浏览器的全视图视觉评审(明暗主题 × 桌面/手机 × 全部六个页面)。找到的 19 个问题全部修复,并由独立的对抗复核确认无一误修。1433 项后端 + 243 项前端测试全绿。
修复(稳定性 / 安全)
- 状态轮询不再可能崩掉进行中的回合:右侧面板的上下文用量显示会高频调用
token_count(),它的 token 备忘录写入与工作线程trim()的字典遍历同处一份数据——特定时机下抛RuntimeError: dictionary changed size during iteration,直接砸断正在流式输出的回合。备忘录现在有了自己的锁,正常聊天 + 状态轮询的组合彻底安全。(73e5c27) - 密钥环写入原子化:
desktop.json存着你所有 provider 的 API key,过去是就地截断重写——写到一半崩溃/断电会清空全部密钥;两个并发的保存操作还会互相丢失对方的条目。现在走临时文件 + 原子替换(0600 权限),所有读改写(含旧版 key 迁移路径)在一把锁下串行。 - 断线重连不再丢帧:角色正在输出时恰好重连,重放窗口内新产生的帧过去既不在重放快照里、也不会实时下发——永久静默丢失。重放现在循环追平后才切换到实时流。
- Linux bwrap 沙盒下
terminal(workdir=…)生效了:过去 jail 参数硬编码切到工作区根目录,模型指定的子目录被静默忽略(npm install跑错地方却毫无提示)。macOS 与 Landlock 一直是对的,现在三层一致。 - 搜索在沙盒被拒时如实报错:过去会静默退化到不经沙盒的纯 Python 文件读取;现在与 terminal 一致——沙盒不可用就明确拒绝,绝不静默降级。
修复(提示词 / harness 设计)
execute_code不再宣传已下架的联网工具:它的模型可见文档还在教web_search/web_extract,但这两个工具早已搁置——模型照着写脚本必然撞墙白烧一回合。文档与真实能力对齐;顺带清掉了沙盒暂存目录里最后一处旧名残留。terminal超时文案与真实行为对齐:超过 600 秒的前台超时实际是截断(附提示),不是文档声称的"拒绝"。- 每回合尾部瘦身:volatile tail 过去每回合重发两句永远不变的工作区介绍(缓存前缀里已有一份)——现在只发真正动态的事实(隔离/网络/日期),长会话省下可观的重复 token。
修复(Web)
- 会话切换的竞态关死:刚销毁的会话的迟到帧过去可能写进下一个会话的消息流;四个即时回调补上了销毁守卫。
- 连接建立前发消息不再报错:先前会弹一条生硬的 "not connected"——现在自动进入持久发送队列,连上即按序送出(气泡即时出现,符合"每次点击立即响应"的原则)。
- 聊天设置的粘性开关(个人网站 / 演员模式 / 沙盒)在快照或角色变化时正确回收,不再显示上一个角色的残留状态。
- Gateways 空状态居中重排,去掉重复按钮。
其余
窄窗口切换后立即裁剪上下文(而非等到下一条消息);请求日志遇到单条超限记录不再自清空;hub 超时的操作会真正取消而非孤儿化;世界书关键词扫描少做一遍全文小写化;现场审计方法论沉淀进 docs/AGENT-FIELD-NOTES.md(含一个会伪装成主题 bug 的无头浏览器渲染伪影)。
Changelog
The second full-sweep hardening, right after v0.1.11: a fresh five-track audit (context core / server / tools / web / prompt & harness design) plus a real-browser visual review of every view × theme × viewport. All 19 findings fixed and independently re-verified by an adversarial review. 1433 backend + 243 frontend tests green.
Fixes (stability / security)
- Status polling can no longer crash an in-flight turn: the side panel's context-usage display calls
token_count()frequently; its token memo writes raced the worker thread'strim()dict iteration — under the right timing,RuntimeError: dictionary changed size during iterationkilled the streaming turn. The memo now has its own lock; chatting with the panel open is fully safe. (73e5c27) - The keyring is written atomically:
desktop.jsonholds every provider API key and used to be truncate-rewritten in place — a crash mid-write wiped all keys, and two concurrent saves could silently drop each other's entries. Now temp-file + atomic replace (0600), with every read-modify-write (including the legacy-key migration) serialized under one lock. - Reconnect no longer loses frames: reconnecting while the character is mid-stream could permanently drop frames produced during the replay window (in neither the snapshot nor the live feed). The replay now loops until caught up before switching live.
terminal(workdir=…)works under the Linux bwrap jail: the jail argv hardcoded a chdir to the workspace root, silently ignoring the requested subdirectory (npm installran in the wrong place with no hint). macOS and Landlock were already correct; all three tiers now agree.- Search fails honestly when the jail is refused: it used to silently degrade to unjailed pure-Python file reads; it now refuses with a visible error, same as the terminal — the isolation ladder never degrades silently.
Fixes (prompt / harness design)
execute_codeno longer advertises the shelved web tools: its model-facing docs still taughtweb_search/web_extract, which are deliberately disabled — the model would write a script straight into a dead end. Docs now match real capability; the last legacy-named staging file is gone too.terminaltimeout wording matches behavior: an over-limit foreground timeout is clamped (with a note), not "rejected" as the schema claimed.- Slimmer per-turn tail: the volatile tail re-sent two never-changing sentences about the workspace every turn (already taught once in the cached prefix) — it now carries only the dynamic facts (isolation/network/date), saving real tokens over long sessions.
Fixes (web)
- Session-switch races closed: a just-disposed session's late frames could write into the next session's message stream; all four immediate callbacks gained the disposal guard.
- Sending before the connection is ready no longer errors: instead of a raw "not connected", the message rides the persisted send queue and delivers once attached (the bubble appears instantly, per the instant-response principle).
- Sticky chat-settings toggles (personal website / actor mode / sandbox) reconcile on snapshot/character changes instead of showing a previous character's leftovers.
- Gateways empty state centered, duplicate button removed.
Everything else
The context trims immediately after a narrow-window model swap (not at the next message); the request log never self-empties on a single oversized record; timed-out hub operations are cancelled rather than orphaned; the world-book keyword scan lowercases the scan text once instead of per entry; the field-audit methodology (including a headless-browser rendering artifact that mimics a dark-theme bug) is recorded in docs/AGENT-FIELD-NOTES.md.
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.
v0.1.10
更新日志
新增
- 市场标签系统重做:从社区角色卡库(character-tavern)筛选角色卡时的标签体系,现在是两个各司其职的列表——热门标签是筛选面板里直接点选的 chip(精简、好扫),而尽量全量的词表(约 300 个真实标签,覆盖题材 / 场景 / 性格原型 / 职业 / 种族 / 关系 / 基调 / 格式)只在手动输入时作为自动补全弹出,不占面板空间。补全中英双向匹配:输
witch出「女巫」、输武士出 samurai、输兔出「兔女郎」;回车自动选中第一条。每个标签都带中文译名,中文界面下 chip 与「已选筛选」都显示中文。词表里没有的标签仍可原文输入照样筛——该库有 700+ 标签,词表只负责发现(点选 / 翻译 / 提示),从不限制可筛的范围。热门 chip 也换成了从库里真实高频标签梳理出来的一组,而非早前那份随手拼的。(fbb470f,cb3c86d,cc33a11,84bc897)
修复
- 空闲时不再对不在场的用户打招呼:自主生活中的一次空闲 tick,本质是一条只带真实世界时钟的用户消息(「没有人在跟你说话,时间在流逝」的约定)。这条约定写在缓存前缀的规则层里,但那个光秃秃的
[时间戳]是模型每个自我工作周期最后看到的用户消息——离前缀很远——于是它屡屡把 tick 误读成「用户来了」,开口就是一句「你来了」。修复把一条即用即弃的提示(没有人在场、不要打招呼或称呼谁、按自己的节奏生活工作)直接折进 tick 本身,并加强了规则层的措辞。该提示搭在临时 tick 上(in_context=False),在持久上下文里不留任何痕迹。(eef4da0)
Changelog
Added
- Market tag system, reworked: the tags for browsing the community card catalog (character-tavern) are now two lists, each with one job — popular tags are the click-to-pick chips in the filter panel (short, scannable), while an aim-for-full vocabulary (~300 real tags across genre / setting / archetype / occupation / species / relationship / tone / format) surfaces only as autocomplete on the manual input, taking no panel space. The autocomplete matches both languages:
witch→ 女巫,武士→ samurai,兔→ bunny girl; Enter picks the top suggestion. Every tag carries a Chinese label, so in the Chinese UI both the chips and the applied-filter chips read in Chinese. A tag missing from the vocabulary is still accepted verbatim — the catalog has 700+ tags, and the dictionary only powers discovery (chips / labels / suggestions), never limits what you can filter by. The popular chips are also now drawn from the catalog's real high-traffic tags rather than the earlier ad-hoc set. (fbb470f,cb3c86d,cc33a11,84bc897)
Fixes
- The chara no longer greets an absent user on idle ticks: a self-work idle tick is, in essence, a user message carrying only the real-world clock (the convention for "no one is speaking to you; time is passing"). That convention is documented in the cached-prefix rules layer — but the bare
[timestamp]is the last user message the model sees each self-work cycle, far from the prefix, so it kept misreading the tick as its user arriving and opening with a greeting ("你来了"). The fix folds a point-of-use note (no one is present; don't greet or address anyone, just live and work at your own pace) into the tick itself and strengthens the rules-layer wording. The note rides the ephemeral tick (in_context=False), leaving zero residue in the durable context. (eef4da0)
v0.1.9
更新日志
新增
- 角色任务层:在「理想」(用户拥有、只读、永不完成的为什么)与会话内的「待办」(即时的步骤清单)之间,补上了缺失的中间层。任务是角色自己长期推进的线索——它自行新增 / 修改 / 封盘,持久化到
task.json,活跃任务搭每个回合的 volatile tail(按回合重算,缓存安全),重启后仍在,活跃上限 12 条。为什么要它:永远够不着的理想给不了抓手,而旧的角色可变目标列表太零散、清空后角色会「罢工」。任务足够具体可落地,但又不是每个角色都能在「数字世界」里完成——所以它的货币是「推进这条线索 + 留下真实的痕迹(一件做出来的东西,或一段做过什么的记录)」,而非「完成一个项目」;完成即封盘(不可变记录),理想的无尽意味着角色永不枯竭,也可以一阵子不持任何任务。价值方向仍只活在用户拥有的理想里——任务纯属工具性。(33558a0,e222062) - 重启 与 删除(角色设置内),取代有缺陷的旧「重新开始这段关系」(清空上下文,行为像半个删除):重启(无害、不确认)——重启角色的子进程,能解开被 bug 卡住的回合,并应用对角色卡的待生效修改(新进程重新读取冻结卡,历史由 make_session 恢复);删除(确认门控、软删除)——角色从已唤醒回到未唤醒,下板、锁定卡下架,数据移入
~/.lunamoth/.trash/(可恢复,带来源清单),绝不抹除,名字释放以便干净地重新唤醒;它唤醒自的那张卡组模板卡原封不动。(5f7b115) - 后台任务完成时主动唤醒角色(live + chat 均生效):一个跑完的后台任务(生图 / 子任务 / 后台终端)现在会唤醒角色去对结果作出反应,两种模式走同一条路——因为一个任务的完成就是一次合成的「用户回合」,正如你说的「工具返回也是 user」。机制:
StateSnapshot.pending_notices做一次廉价的非破坏性窥探;agent.stream_react()把待处理通知排进上下文并跑响应式回复循环;dispatch 的react是像idle一样的低优先级流,真实的用户发送 / 事件永远抢占它、它永不抢占真实回合(不会卡死流锁);supervisor 空闲循环在模式分支之前、当有待处理通知且无客户端在流时,先重读最新快照再驱动一次性 react。(cfc4a67) - 持久化参考图(参考图)进素材库:视觉编辑器的参考图托盘过去是临时的(每次生成上传、从不保存),现在持久化进卡片素材库(打开时加载卡片的素材图,增删即写入),离开视图也留得住、持续指导生成。Tavern 导入(PNG 拖入 + 市场封面)现在把立绘同时落为一张持久化参考图(修正了早前误塞进主视觉的问题)。(
a82ad67) - 持久化发送队列 + 流排序:回合进行中输入的消息会排队并持久化(重新挂载时恢复,回合末逐条一回合一条地放出);attach 期间到达的流帧先缓冲、在历史之后按序回放。视觉编辑器:删除一张已持久化的参考图前先确认(不可逆的素材解链),并悬停露出其移除(×)按钮。(
bf65487) - 档案页可折叠 + 文案瘦身:任务 / 技能 / 记忆三个分节现在可折叠(记忆默认收起,它可能是一大段;理想保持常开),用与卡组一致的大写标题开关 +
›箭头(展开旋转向下)+ 轻淡入,亮 / 暗皆宜。文案去掉解释性废话——技能空状态就是「No skills yet.」,任务空状态就是「还没有任务 / No tasks yet」。(6c08ab6,e47b3d4)
修复
- 子任务(delegate)不再阻塞 + 强制每子超时;重新启用:
delegate_task不再阻塞回合,而是像生图一样作为后台任务运行——扇出跑在守护线程上、经完成队列汇报,调用立即返回{status: submitted}回执,子代理与主代理并肩工作,聚合结果在下个回合边界作为合成用户消息排入。每子超时现在真正强制(此前已定义却从未生效,正是线上「卡住 / 一个流已在进行中」挂起的根因):_run_fanout始终走线程池 +fut.result(timeout=…)→ 真实的timed_out结果 +shutdown(wait=False),挂死的 worker 再也无法二次阻塞返回。(aec7516) - 存储原子写 + 按 id 裁剪:理想与任务存储改为原子写(mkstemp → fsync →
os.replace),杜绝写一半的损坏文件;完成任务的裁剪改用稳定 id 而非对象id(),并清掉几处过时的wish/愿望 注释。(b456a22) - CI:发布工作流的 Node-20 动作升级到 Node-24(setup-uv v6→v8.2.0、action-gh-release v2→v3)。(
12c7233)
Changelog
Added
- Chara task layer: the middle layer the aspiration/goal split was missing — between the aspiration (the user-owned, read-only, never-completed why) and the in-session todo (the immediate step checklist). A task is the chara's own thread it carries over time: it adds / edits / seals its own, persisted to
task.json, active ones riding every turn's volatile tail (recomputed per turn → cache-safe), surviving restarts, capped at 12 active. Why: a never-reachable aspiration gave no traction, and the old chara-mutable goal list was scattered and the chara "went on strike" once it emptied. A task is concrete enough to act on yet not digitally "completable" for every character — so the currency is "advance the thread + leave a real trace in whatever form fits your world", not "complete a project". Completion seals a task (immutable record); the aspiration's endlessness means the chara never runs dry, and it may keep none for a while. The value-direction still lives only in the user-owned aspiration — a task is purely instrumental. (33558a0,e222062) - Restart and Delete in chara settings, replacing the flawed old "clear-context" (which behaved like a half-delete): Restart (harmless, no confirm) bounces the chara's child process — unblocks a turn wedged by a bug AND applies pending card edits (the fresh process re-reads the frozen card; history is restored by make_session). Delete (confirm-gated, soft): the chara returns from awakened to not-awakened — off the board, its locked card off the deck — and its data is moved to
~/.lunamoth/.trash/(recoverable, with an origin manifest), never erased; the name is freed for a clean re-wake. The deck template card it was woken from is untouched. (5f7b115) - Proactively wake the chara on background-job completion (live + chat): a finished background job (generate_image / delegate / background terminal) now wakes the chara to react to it, in both modes, via one shared path — because a job finishing is a synthetic USER turn, exactly your framing ("a tool return is also a user message"). Mechanism:
StateSnapshot.pending_noticesis a cheap, non-destructive peek;agent.stream_react()drains pending notices into context and runs the responsive reply loop; the dispatchreactis a low-priority stream likeidle— a real user send / event always supersedes it and it never supersedes a real turn (no way to wedge the stream lock); the supervisor idle loop, before the mode branch, re-reads a fresh snapshot and drives a one-shot react when notices are pending and no client is streaming. (cfc4a67) - Persisted reference images (参考图) in the asset library: the visual editor's reference tray was ephemeral (uploaded per generation, never saved) — it now persists in the card's asset library (loads the card's asset images on open; add/remove write through), so a reference survives leaving the view and keeps guiding generation. A tavern import (PNG drop + market cover) now lands its portrait as the sprite (立绘) AND a persisted reference (参考图), fixing the earlier mis-slotting into the keyvisual. (
a82ad67) - Persisted send-queue + stream ordering: messages typed while a turn is in flight are queued and persisted (restored on remount, flushed one-per-turn at turn end); stream frames arriving during attach are buffered and replayed in order after history. Visual editor: confirm before deleting a persisted reference image (an irreversible asset unlink) + a hover-revealed remove (×) button. (
bf65487) - Collapsible Profile sections + trimmed copy: Tasks / Skills / Memory now fold away (Memory defaults closed — it can be a wall of text; Aspiration stays always-open), with an on-brand uppercase-header toggle + a
›chevron that rotates down when open + a soft reveal, light & dark. The copy drops its explanatory padding — the skills empty state is just "No skills yet." and tasks-empty is just "还没有任务 / No tasks yet". (6c08ab6,e47b3d4)
Fixes
- Subagent (delegate) no longer blocks + enforced per-child timeout; re-enabled:
delegate_taskno longer blocks the turn — it runs as a background job like generate_image (the fan-out on a daemon thread, reporting via the completion queue), the call returns a{status: submitted}receipt immediately, the subagents run alongside the main agent, and their aggregated results drain as a synthetic user message at the next turn boundary. The per-child timeout is now enforced (it was defined but never applied — the root of the production "stuck" / "a stream is already in flight" hang):_run_fanoutis always pooled and usesfut.result(timeout=…)→ a realtimed_outresult, withshutdown(wait=False)so a hung worker can't re-block the return. (aec7516) - Atomic store writes + trim-by-id: the aspiration and task stores now write atomically (mkstemp → fsync →
os.replace), eliminating a half-written corrupt file; completed-task trimming keys off a stable id instead of objectid(); plus a sweep of stalewishcomments. (b456a22) - CI: the release workflow's Node-20 actions moved to Node-24 (setup-uv v6→v8.2.0, action-gh-release v2→v3). (
12c7233)
v0.1.8
更新日志
新增
- 角色市场 v2:从一个搜索框升级为真正的目录。修正封面图床(改用带实时缩略的存储 CDN,封面终于能加载);进入即浏览真实榜单,支持「热门 / 趋势 / 最新」排序,空查询也可浏览;筛选:标签 chips(逗号连接做 AND)、原创(OC)、含世界书、NSFW,每张卡带下载 / 喜欢 / 对话角标,「加载更多」分页;点开卡片是只读预览(封面、统计、标签、人设分节),样式与浏览自己的卡一致,只有「加入卡组」一个动作。(
9ec4aa9) - 忠实的卡片导入(不经模型):人设 / 开场白 / 世界书逐字带入。可在创建框粘贴外部卡 JSON(ST V2/V3 data 块、V1 扁平结构,或 character-tavern 的 API 结构),或直接把
.json/.png卡拖进来——PNG 卡内嵌的立绘会作为角色美术带过来。Tavern 美术(拖入的 PNG + 市场封面)落为立绘,绝不占用头像;头像 / 表情 / 背景留给你自己生成。对缺失项同样宽容:没有理想就省略、主题色确定性推导、字典形式的世界书条目按原生加载器归一化、悬空的素材指针丢弃。(9ec4aa9) - 提示词:用户语言 + 简洁 + Fable-5 中立工艺(仅在启用工具时叠加,纯角色扮演卡不受影响):用用户书写的语言回答;回复长度匹配问题——简单的问题给短答案,而非长篇大论 / 复述 / 收尾;独立产物写进工作区文件(
MEDIA:),可阅读的东西内联呈现;引用文件前先确认其存在而非杜撰内容;需求不明确时先做一版、说明假设、只问一件事;相信新鲜的网络结果胜过过期记忆;共享文件之后不再长篇复述。(a0ae089)
修复
- 去除提示词中的品牌泄漏:工具包
note随稳定前缀注入、是模型可见的——从内置 sandbox 包(note 与面向操作者的描述)里清掉(hermes parity)/hermes tool surface字样,并修正一处过时的wish(该工具已移除,角色生活工具是 speak / rest)。新增回归测试扫描内置包是否有品牌泄漏。(a0ae089)
Changelog
Added
- Market v2: from a search box to a real catalog. Covers fixed (the cover host 403'd everyone — switched to the storage CDN with on-the-fly thumbnails, so covers now load); opens to a default browse over the real ranking with Popular / Trending / Newest sort tabs (empty query allowed); filters — tag chips (AND via one comma-joined param), Original (OC), Has lorebook, NSFW — plus per-card download / like / message badges and "load more" pagination; clicking a card opens a read-only preview (cover, stats, tags, persona sections) styled like browsing your own cards, with one action: add to deck. (
9ec4aa9) - Faithful card import (no model call): persona / greeting / lorebook brought over verbatim. Paste a foreign card JSON in the create box (ST V2/V3 data block, V1 flat, or the character-tavern API shape), or drag a
.json/.pngcard in — a PNG card's embedded portrait is carried over as the character's art. Tavern art (a dropped PNG + the market cover) becomes the sprite (立绘), never the avatar; the avatar / stickers / background are left for you to generate. Tolerant of what foreign cards lack: absent aspiration omitted, theme color derived deterministically, dict-form lorebook entries normalized like the native loader, dangling asset pointers dropped. (9ec4aa9) - Prompt: reply in the user's language, brevity, + Fable-5 neutral craft (tools-gated — a pure-roleplay card is untouched): answer in the language the user is writing in; match response length to the question — a simple ask gets a short answer, not an essay / recap / wrap-up; route a standalone artifact to a workspace file (
MEDIA:) and a thing-to-read inline; verify a referenced file exists before acting rather than inventing contents; on an underspecified request, take a first pass, state assumptions, ask one thing; trust a fresh web result over stale memory; let a shared file speak without a long postamble. (a0ae089)
Fixes
- De-brand the prompt: the toolpack
noterides the stable prefix and is model-facing — dropped the(hermes parity)/hermes tool surfacebrand from the bundled sandbox pack (both the note and the operator-facing description) and fixed a stalewish(that tool was removed; the chara-life tools are speak / rest). A new regression test scans the bundled pack for brand leakage. (a0ae089)
v0.1.7
更新日志
新增
- 角色市场:新增顶层「市场」板块(角色标签页),可搜索开源的 character-tavern 目录,一键把卡片加入卡组。后端是按需直连其公开 JSON API 的轻代理(hub 跑在你自己机器上,无需托管中转);导入会把外部 V2/V3 ST 卡映射到我们的卡片结构、写进卡组。导入对外部卡的缺失很宽容——没有理想就省略、没有主题色就按卡确定性地推导一个(绝不留下会让卡组崩溃的「无主色」主题)、
{{char}}/{{user}}宏原样保留、世界书(character_book)透传到内嵌世界;NSFW 有门控,失败给出真实错误而非空白。(8d8ef67) - 封面随卡过来:导入市场卡时真正把图带进卡组——网页客户端在导入后抓取封面字节并上传(
card.asset_save主视觉 + 头像),得到一份本地副本(离线可用,存在即优先);若被 CORS 拦截,仍保留封面 URL,由卡面<img>浏览器侧加载兜底。封面在你于市场里看得见的时候就一定跟着卡走。(31270f8)
修复
- 封面优雅降级:character-tavern 的图床对非浏览器请求做防盗链(403),服务器侧抓不到——网格缩略图改为浏览器侧
<img>,加载失败干净地退回字母组合图(monogram),市场永不显示破图;封面 URL 保存在导入的卡上以便浏览器侧展示。服务器侧附图仍尽力而为(带浏览器式请求头 + 真实图片格式探测),并在跳过时记录原因而非静默。(38e27c1) - 卡片路径编码:卡片路径可能含空格 / unicode(如
bmboster/Yae Miko),之前未编码会让 URL 出错——现对每段做百分号编码,同时保留原始路径作为存储身份。(9963d1f) - 导入回归收口:封面失败状态改由 React state 驱动(重复搜索同一张卡不再永久隐藏后来可加载的封面);刷新 toast 移出导入
try(导入成功后不再误报红色错误);路径在任何抓取前先拒绝./..段;NSFW 门控再加一层标签匹配,按路径直接导入也无法绕过。(be09116)
Changelog
Added
- Character Market: a new top-level Market section (Characters tab) — search the open character-tavern catalog and add a card to your deck in one click. The backend is a thin, on-demand proxy to its public JSON API (the hub runs on your own machine — no hosted relay); import maps a foreign V2/V3 ST card onto our card shape and writes it into the deck. Import is tolerant of what a foreign card lacks — no aspiration → omitted, no theme color → a deterministic per-card color (never a primary-less theme, which used to crash the deck),
{{char}}/{{user}}macros left intact, the lorebook (character_book) passed through to the embedded world; NSFW is gated and a failure surfaces a real error, never a blank. (8d8ef67) - The cover follows the card on import: importing a market card now actually carries its image into the deck — the web client fetches the cover bytes after import and uploads them (
card.asset_savekeyvisual + avatar) for a local, offline-safe copy (wins when present); if that's CORS-blocked, the stored cover URL still renders browser-side via the card-face<img>. The cover follows the card whenever you could see it in the market. (31270f8)
Fixes
- Graceful card covers: character-tavern's image CDN hotlink-protects non-browser fetches (HTTP 403), so the hub can't download covers server-side — the grid thumbnails are browser-loaded
<img>that degrade to a clean monogram on error, so the market never looks broken; the cover URL is preserved on the imported card for browser-side display. The server-side attach stays best-effort (browser-like headers + real image-format detection) and now logs WHY it skipped instead of failing silently. (38e27c1) - URL-encode card paths: a card path can contain spaces / unicode (e.g.
bmboster/Yae Miko), which we passed unencoded into URLs (urllib would choke) — now each path segment is percent-encoded while the raw path stays as the stored identity. (9963d1f) - Import regressions closed: cover-load failure is now driven by React state (re-searching the same card no longer leaves a now-loadable cover hidden forever); the refresh toast moved out of the import
try(no red error after a successful import); paths reject./..segments before any fetch; the NSFW gate is backed by a tag match so a direct import-by-path can't slip past the search-side exclude. (be09116)
v0.1.6
更新日志
修复
- 网页端聊天历史显示不全:历史被截断了两层——前端只渲染最后约 80 行(含工具/思考行,≈15–25 轮对话),且后端只显示最近一次上下文压缩之后的内容。现在网页端显示完整的近期对话。这同时解决了"thinking 块漂到顶部"的现象(截断窗口恰好落在压缩后的自我工作/思考区)。(
afecbdb)
新增
- 压缩边界分隔线:发生上下文压缩后,网页端在边界处显示一条很轻的分隔线"以上对话已压缩进记忆"——原始对话仍完整可见,分隔线只是诚实地告诉你:这条线以上的内容,角色是概要记忆而非逐字记得。模型的上下文/缓存不受影响。(
afecbdb)
Changelog
Fixes
- Incomplete webui chat history: history was truncated twice — the frontend rendered only the last ~80 rows (tool/think rows included → ~15-25 exchanges), and the backend showed only what survived the latest context compaction. The webui now shows the full recent conversation. This also resolves "thinking blocks drifting to the top" (the truncated window started in a post-compaction self-work / thinking region). (
afecbdb)
Added
- Compaction-boundary divider: after a context compaction, the webui marks the boundary with a subtle divider — "earlier conversation condensed into memory". The raw turns stay fully visible; the divider just tells you honestly that above it the chara holds a summary, not the verbatim turns. The model's context/cache is unchanged. (
afecbdb)