cmagent v0.1.0
Changelog
Notable changes by release. Date format YYYY-MM-DD.
v0.1.0 -- 2026-05-09
Initial public release.
Platforms: Linux x86_64 / aarch64, macOS x86_64 / aarch64, Windows x86_64.
Highlights
- Multi-provider agent loop (Anthropic, OpenAI-compatible, GLM) with SSE streaming
- 20+ built-in tools: file operations, shell, web fetch/search, CDP browser automation,
persistent memory (brain), sub-agent delegation, unified messaging - Channel integrations: Telegram, Lunkr (p2p + group), Slack, Discord, WeChat
- SKILL.md-based prompt extensions with keyword/tag activation and slash command support
- 4-layer security model: input guard, application policy, OS sandbox, output safety
- Gateway (HTTP + WebSocket, multi-user RBAC), ACP stdio server, Ralph Loop
- Config wizard,
cmagent doctor, session undo/redo/retry
Development log (pre-release)
[Unreleased] -- 2026-05-01
Preserve reasoning_content across turns (DeepSeek V4 thinking mode)
DeepSeek V4 returns a reasoning_content field on assistant turns
(thinking mode) and rejects follow-up requests that don't echo the
prior turn's reasoning chain back:
API error 400: The reasoning_content in the thinking mode must
be passed back to the API.
The OpenAI provider was stripping reasoning entirely when
serializing message history. Three changes round-trip it:
-
ChatMessagegainedreasoning_content: Option<String>
(skip-serializing-if-none for back-compat with old session
JSON and providers that don't accept the field). -
New constructor
ChatMessage::assistant_with_tools_and_thinking;
the agent's turn-loop now uses it for both tool-call turns and
final-answer turns so every prior assistant message carries its
reasoning. -
The OAI request body serializer
(OpenAiProvider::build_request_body) only includes
reasoning_contenton outgoing messages when the model is in
the round-trip allowlist (requires_reasoning_round_trip,
currentlydeepseek-v4-*). Other thinking-mode models on the
OpenAI-compatible wire (DeepSeek R1's docs say to omit
reasoning_content; GLM / Qwen behavior unverified) are
unaffected -- they still get the prior pre-fix behavior of no
reasoning in messages. Storage inChatMessagehappens
regardless so the data survives a model switch within a
session.Additional case: legacy session histories saved before
ChatMessagegained the field have None on every assistant
turn. For V4 the field MUST be present (DeepSeek rejects
follow-ups whose prior assistant turn lacks it), so the new
build_reasoning_content_for_messagehelper emits an empty
string in that case -- the field is there, just empty, which
V4 accepts. Without this fallback, switching an existing
session to V4 would 400 on the first turn.
Provider catalog: deepseek-v4-pro / deepseek-v4-flash flipped
to supports_thinking = true to reflect actual V4 behavior.
Tests:
crates/cmagent-provider/src/types.rs::tests(4):
assistant_message_omits_reasoning_when_absent,
assistant_with_thinking_preserves_reasoning,
assistant_with_empty_thinking_drops_field,
chat_message_deserialize_back_compat.crates/cmagent-provider/src/openai.rs::tests(1):
round_trip_only_for_deepseek_v4locks in the allowlist
so other models (R1, V3, GPT, GLM, Qwen) stay opt-out.
/model picker: read providers from disk on every open
Editing a provider via the in-TUI /config wizard wrote the new
config to ~/.cmagent/providers/*.toml but the subsequent
/model picker still showed the snapshot loaded at TUI startup.
Users had to exit and re-launch to pick the new model. Both ends
fixed:
tui2/key_handler.rs::UserCommand::Modelnow does
ConfigLoader::from_default().load_providers()on each open
(matching the/agentpicker's existing fresh-load pattern)
instead of using the cachedopts.providerssnapshot.agent/commands.rs::handle_command(UserCommand::Model)reloads
providers_configfrom disk before resolving the new
provider_id:model_id. Without this, edits tobase_url/
api_key_envmade via/configwould be ignored on switch
because the agent's snapshot was taken at build time.
DeepSeek provider catalog
Add the V4 lineup (deepseek-v4-pro, deepseek-v4-flash) to
assets/provider_catalog.toml per the model ids returned by the
DeepSeek API. deepseek-v4-pro is the new default; flash ships
as a cheap-tier alternative. Old deepseek-chat (V3 alias) and
deepseek-reasoner (R1) entries kept for backward compatibility.
Pricing is a placeholder until DeepSeek publishes official numbers.
Doctor: provider key diagnostics
cmagent doctor now prints the masked api key + length next to
each provider's "set" line so users can sanity-check the key
cmagent loaded against what their dashboard shows. Catches stale
.env files and copy-paste truncations that the bare "is set"
line previously hid. Trailing whitespace also gets a dedicated
warning since load_env_file already trims it but the dashboard
might not.
Lunkr p2p: keyboard response classifier bound to fid=1212
The Lunkr server moved inline keyboard responses from fid=1211
(multiplexed with the chat-window-opened P2pPing) to fid=1212
(dedicated channel) in the 2026-04-27 protocol revision. The
classifier in cmagent_channels::lunkr::p2p previously matched
attachments[0].t == 22 on any fid -- behavior happened to keep
working under both protocols, but the loose check would also
classify any future stray t=22 payload on an unrelated fid as
a keyboard response.
p2p::classify now requires the canonical wire shape
(fid==1212 && attachments[0].t==22) for the KeyboardResponse
arm. Legacy fid=1211 + t=22 payloads fall through to
OtherSignal; bare t=22 with no fid falls through to
NotSignal. The fid==1212 branch documents the exact line to
widen if a mixed-server environment ever needs to bridge to
older Lunkr backends. [9ba5b9f]
Tests added/updated: test_classify_keyboard_response (now
includes fid=1212), test_classify_keyboard_response_legacy_fid_1211_no_longer_matches,
test_classify_keyboard_response_no_fid_no_longer_matches, and
test_classify_keyboard_response_priority_over_other_p2p_meaning.
Doc updates: crates/cmagent-channels/src/lunkr/p2p.rs module
header (fid layout table), keyboard.rs module header (wire
format note), and the dispatch comment in lunkr/mod.rs all
describe the new fid layout.
[Unreleased] -- 2026-04-22 to 2026-04-24
Highlight: Unified messaging tools
Replaced the per-channel outbound tool surface (lunkr_send_message,
telegram_send_message, <kind>_search_contacts, ...) with two
action-dispatched tools:
messaging_query(Low risk) -- read surface:list_channels,
describe_channel,search_contacts,list_chats,
list_messages,download_attachment.messaging_send(Medium risk) -- write surface:send_message,
send_file,edit_message,delete_message,send_buttons,
notify, plus platform-specific actions (send_embed,
open_modal,create_thread,add_friend,accept_friend,
send_pat).
Channels: Lunkr, Telegram, Slack, Discord, WeChat. The LLM uses
describe_channel to learn what each adapter supports before
attempting a call. Inbound channel sessions are scoped: every action
except notify is restricted to the originating channel.
Design doc: docs/plans/2026-04-24-unified-messaging.md.
Added
OutboundChannel::describe()returning aChannelDescriptor
capability sheet with structured per-action support objects (e.g.
send_buttons.wait_response = true). [03c863b]MessagingRegistrykeyed by channel kind with shared
Arc<dyn OutboundChannel>and operator-sideChannelMeta
(display_name,outbound_enabled,notify_target). [1028495]messaging_send.send_buttonsacceptswait_response: true+
timeout_seconds; Lunkr's p2p bridge backs the blocking flow.
[6d1db12]messaging_query.search_contacts/list_chats/list_messages
fully wired through to adapter methods (commit 3/5 had left them
asnot_implementedstubs). [b3f1cdf]- Centralised
cmagent_channels::outbound::CHANNEL_KINDSconstant;
removed five duplicated copies. [d2e11c1] - Channel scope detection in
src/infra.rs::build_agent(CLI / TUI
path) so resuming alunkr-*session enforces the same scope as
the gateway path. [d2e11c1] - Lunkr clone-role self-send guard (
check_self_send) with a clear
error message; only the operator's exact uid is rejected, not
every#Urecipient. [2d8398c] - Discovery wires
messaging_query/messaging_sendinto
discover_tool_names()so the config wizard andcmagent doctor
can see them. [48090b0] - Design document for the rollout:
docs/plans/2026-04-24-unified-messaging.md.
[1f57984]
Changed
- Shipped agent profiles migrated:
chataddsmessaging_query
(read-only);codingandadminadd both tools. [6b545cc] - Lunkr / Telegram / Slack / Discord / WeChat registrations now
prefer the account withallow_outbound_send=trueover the first
enabled account so opt-in always wins regardless of TOML order.
[d2e11c1, 2d8398c] messaging_sendparameters schema documentstrigger_id,
target,message,request_id,wait_response,
timeout_seconds;refwidened to accept string OR object for
Lunkr's compound attachment shape. [d2e11c1]- Telegram / Slack / Discord / WeChat are registered in the
messaging registry even withoutallow_outbound_sendso
inbound-only sessions can still calldescribe_channel; send
actions stay gated byoutbound_enabled. [d2e11c1] cmagent doctorwarns when an agent's explicit tool allowlist
excludesmessaging_sendwhile at least one channel has
allow_outbound_send=true. [b0b41d7, 6b545cc]
Removed
- All
<kind>_send_message,<kind>_send_file,
<kind>_search_contacts,<kind>_list_chats,
<kind>_list_messages,<kind>_notifyper-channel tools. [6b545cc] lunkr_download_file(replaced by
messaging_query.download_attachment). [6b545cc]lunkr_send_keyboard(replaced by
messaging_send.send_buttons { wait_response: true }). [6d1db12]cmagent-tool::builtin::outboundmodule (legacy tool factories).
[6b545cc]- Synthesized
CHANNEL_OUTBOUND_HINTS.mdagent.md section --
unified tools always register, sodescribe_channelalready
surfaces availability. [6b545cc]
Other
TUI
/status"Core" / "Extended" tool lists word-wrap to terminal
width via the new[status:list]INDENT|LABEL|itemsmarker.
Narrow windows no longer drop tools off the right edge. [fe0eebe]- Streaming thinking preview collapses embedded newlines and word-
wraps long lines (CJK and ASCII alike). Continuation lines align
to the tree gutter. [e5f8632]
Outbound config wizard
Enable outbound send?prompt added to both new-account and
edit-account flows. Toggling off clears stalenotify_*keys.
[283782a]- Wizard now lists 5 outbound-capable channels (Lunkr, Telegram,
Slack, Discord, WeChat) consistently via the central constant.
[283782a, d2e11c1] - Workspace prompt in channel edit is optional rather than
required (previously refused to advance with an empty value).
[ba4076c]
Lunkr
- Image attachments are downloaded inline into
ChannelEvent.images
so vision-capable providers see them natively. [5a815c7] - Downloaded images persist under the workspace
.cmagent/dl/lunkr/
tree, namespaced by account so multiple accounts don't collide.
[1f24e36]
Vision
- Native-first fallback: when a provider exposes vision, images
ride along inChatMessage.images; otherwise thevisiontool
is registered as a fallback. Telegram image attachments are now
preserved end-to-end. [d290408]
Doctor
- Detects legacy
[security]schema (autonomy,max_tool_risk,
separate path fields) in user configs and points to the new
field names. [74016f0]
[Unreleased] -- 2026-04-21 to 2026-04-22 (security model refactor)
Three-phase refactor of the agent security schema. Final shape is
documented in docs/security-model.md.
Changed
max_tool_risk->max_skill_risk(gates skills + MCPs only,
not the tool allowlist). Path fields (allowed_paths,
workspace.extra_dirs,sandbox_extra_read_paths,
sandbox_extra_write_paths) consolidated into a single
extra_dirs = [{ path, mode }]list. [d3319df]- Skill / MCP filtering enforces the risk ceiling at runtime
rather than only at registration time. [16a72b2] autonomy->prompt_thresholdwith three values:medium
(ask for medium+high risk),high(ask for high only),never
(autonomous). [87aa5e0]
Added
docs/security-model.md: full description of the four-layer
security model and the runtime gate. [619867a]cargo fmt --allbaseline applied across the workspace. [422ca8b]- Hermes Agent research notes; Python cache added to
.gitignore.
[6db384b]