# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.5.3] - 2026-09-28 ### Fixed - Restore `/session`, `/current`, and `/resume` on Harness 0.1.7-rc.2 by normalizing both legacy session headers and the newer persistence snapshots. - Preserve stored working directories during startup and resume existing sessions instead of treating them as missing. `/new` observes the highest stored epoch even when another driver created it after startup. - Read cold session logs with `open(..., 'read')` on the current API, release handles after success or failure, and retain the legacy `inspect()` path. - Normalize snapshot entries in the legacy status-route helper as well. ### Tests - Run history command regressions against both persistence shapes and verify read-handle cleanup on failures and cancellation. - Add a CI smoke test using the real rc.2 JSONL backend and temporary sessions, covering cold titles, workspace regrouping, routing across restarts, and epoch allocation without connecting to WeCom or calling a model. - Bound the persistence peer declaration to the two covered API releases. ## [0.5.2] - 2026-09-28 ### Changed - Publish the session-navigation and streaming-reliability updates documented in 0.5.0 and 0.5.1 to npm. - Document the current channel-only architecture: status and reconnect services remain available, while a separate dashboard owns the web UI and HTTP routes. Upgrades from 0.3.x no longer receive the bundled status panel or bot manager. - Refresh English and Chinese installation, upgrade, multi-bot, and approval documentation; replace deployment-specific test examples with generic data. ### Fixed - Complete plugin initialization while the WebSocket loop runs in the background, making the status service available to other plugins. - Format the anonymized model fixture so the full CI check passes. ## [0.5.1] - 2026-09-22 ### Fixed - **Streaming went silent after the harness 0.1.6 upgrade.** The pool watched `session/event` for `assistant/chunk` to drive both the live WeCom stream and `Reply.reasoning`, but that event no longer exists: the live model-delta feed is now the transient `agent/assistant-stream` event (`Agent`-scoped frames carrying `text-delta` / `reasoning-delta`), while `assistant/chunk` only survives as a V0-log replay shape. The turn therefore emitted no delta at all — the chat showed `Working…` until the final `finish=true` frame delivered the whole answer at once, and the final `` card carried tool calls but no reasoning. The pool now subscribes to `agent/assistant-stream` and keeps the `assistant/chunk` branch as a legacy fallback that is ignored once real stream frames are seen (so a harness emitting both cannot double-count). - **A thinking-only turn could be killed as stuck.** The no-progress watchdog reset its deadline on `session/event` alone, but a long reasoning pass with no tool call emits only `agent/assistant-stream` frames. Such a pass looked like silence and was cancelled with "agent response timed out". The watchdog now treats stream frames as progress too. - **Rendered cards were dropped whenever the final stream frame failed.** Card delivery shared the passive stream's `try`/`catch`, so a report turn that outlived the ~6-minute stream window (errcode 846608) fell back to the proactive text push and skipped every image — the user got the written report with no card. Cards now ship on their own path after the text lands by whichever route succeeded. ### Added - Turn failures in the stream fallback and card-send paths are recorded to the `DSH_WECOM_ERROR_LOG` file (`/tmp/dsh-wecom-error.log` by default), matching the existing turn-failure sink — the plugin logger never reaches the service journal, which is why these failures were invisible in production. ## [0.5.0] - 2026-09-02 ### Added - **Session history navigation** — three new bot commands for browsing and switching between a chat's past sessions. Every `/new` mints the next epoch (`base~gN`) while the older sessions stay persisted and visible in the web sidebar, so a chat accumulates one resumable session per epoch; until now there was no way back except the web UI. - `/session` (alias `/sessions`) lists this chat's sessions newest-first — epoch number, start time, completed turns, and the latest title — with the session new messages route to marked `*`. Only the chat's own base id and its `~gN` epochs are listed (other chats and web sessions never leak in). - `/current` shows the one session new messages currently route to. - `/resume ` switches the chat's routing pointer to epoch `n` (the number shown by `/session`; `0` is the original session). The selection is persisted to `.dsh-wecom-state.json`, so it survives restarts exactly like `/new`. Only sessions that exist and are not archived in the web UI may be resumed; a running turn on the previous session is left to finish, and the switch applies from the next message on. Resuming works across a restart too — the epoch comes back from the state file and the session is resumed from persistence rather than recreated. - `SessionView` is exported for channels/UIs that want the structured rows. ### Fixed - `/new` after `/resume` no longer collides with an existing session. The fresh epoch is now one past the HIGHEST epoch ever minted for the chat (previously "current + 1"): rewinding the routing pointer to an older epoch and then starting fresh used to reuse an existing `~gN` id and silently continue its old history instead of opening a new session. ## [0.3.0] - 2026-08-25 ### Fixed - Rendered image cards now actually reach the WeCom chat. The upload path was silently broken: `attachments.readImage()` returns a plain `Uint8Array`, but the SDK's `uploadMedia(fileBuffer)` expects a Node `Buffer` and encodes each chunk with `chunk.toString('base64')` — on a `Uint8Array` that call does NOT produce base64 (it yields `"[object Uint8Array]"`), so WeCom reassembled garbage bytes, never matched the declared `total_size`, and rejected every upload with `40006 invalid file size`. Cards are now wrapped with `Buffer.from()` before uploading. Verified end-to-end: cards arrive as image messages on the proactive channel (`aibot_send_msg` + media_id). ### Changed - Card delivery no longer uses stream `msg_item` at all: the WeCom long-connection document states the passive stream reply does not support `msg_item`, and inlined base64 images were blocked by the platform content-security gate ("未通过安全检查的图片内容"). The answer text rides the stream; each card is uploaded to the media library and pushed as a dedicated `msgtype: image` message on the proactive channel — the documented image path. ## [0.2.1] - 2026-08-24 ### Fixed - Approval acknowledgements (「已批准,继续执行。」/「已拒绝…」) are now sent through the PROACTIVE channel (`sendMessage`) instead of opening a second passive reply stream. Production trace showed the conversation's own reply stream (opened with `Working…` and finished after the tool retry) never reached the user when an approval ack fired as another passive `finish=true` stream on the same chat mid-turn — the ack displaced the pending reply. The proactive channel is fully independent of passive streams, so both always deliver. - The final `finish=true` reply now falls back to a proactive push when the passive stream rejects it (e.g. the 6-minute stream window expired with errcode 846608, or the stream was displaced). Previously a failed final frame silently dropped the finished answer; the error-path reply got the same fallback. ## [0.2.0] - 2026-08-24 ### Added - Rendered image cards can now be sent to the WeCom chat. When an agent turn renders a card (e.g. the `render_card` tool), the pool collects the durable image attachments from the tool-result events into `Reply.images`, and the channel materializes them as image `msg_item` entries (base64 PNG + MD5) on the final stream frame. The SDK allows `msg_item` only on `finish: true` and at most 10 images; a card whose bytes cannot be read is logged and skipped. ## [0.1.29] - 2026-08-24 ### Added - Sandbox-escalation approvals are now answerable inside WeCom chats (`approvalMode: 'chat'`, the default): when a WeCom agent's tool retries with wider permissions, the harness approval request is claimed by the new `ApprovalBridge`, pushed into the chat that triggered it (tool name + reason + reply hint), and resolved from the chat's next reply word (批准/同意/允许/ok/yes to allow; 拒绝/不批准/不同意/no to reject). Unanswered asks fail closed to `cancelled` after `approvalTimeoutMs` (default 5 min). Reply words are intercepted before the normal message flow, so they answer the waiting turn instead of queueing behind it. The bridge registers on the approval waterfall with `prepend`, so the web-UI answerer never races it. - `approvalMode: 'notify'` pushes the ask into the chat but leaves the decision to the web UI; `approvalMode: 'off'` restores the old silent behavior entirely. - `approvalAllowlist` (userids) restricts who may answer an in-chat approval; empty admits every sender the channel already admits. Foreign chats' reply words never resolve another chat's pending approval. ## [0.1.28] - 2026-08-22 ### Added - Workspace deletions made in the web UI now stick: a runtime watcher polls the workspace registry and records each deleted per-chat row as a tombstone in `.dsh-wecom-state.json` (`deletedWorkspaces`), so the startup regroup no longer recreates rows the user deleted. A fresh message on a tombstoned conversation clears the tombstone and recreates its row, so an accidental deletion self-heals on the next activity. The poll interval is `AgentPool.DELETION_POLL_MS` (5 s; overridable in tests). ## [0.1.27] - 2026-08-21 ### Changed - `replyLimitBytes` schema ceiling raised from 20,480 to 204,800 bytes: long reasoning + tool-heavy replies no longer need the `[reply truncated]` marker so soon; deployments can raise the limit beyond 20 KB. ## [0.1.26] - 2026-08-21 ### Changed - Turn settling uses a no-progress timeout instead of a fixed total budget: the deadline resets on every session event (reasoning deltas, tool calls, assistant text), so long thinking passes and slow tool loops stay alive while demonstrably moving; only a turn that goes silent for `turnTimeoutMs` is cancelled, and that still surfaces as an error so the user knows to retry. ### Fixed - pnpm v11 install on the repo now approves esbuild's build script via `pnpm.onlyBuiltDependencies`, unbreaking fresh checkouts. ## [0.1.25] - 2026-08-21 ### Added - Per-session sandbox cwd: every WeCom conversation (each `/reset` epoch is its own session id) runs in its own directory under the configured base — `WeCom-{userid|chatid}-{MMDD}-{hhmmss}-{id-tail}` — so the harness sandbox fence isolates each session's filesystem, and one workspace row appears per session with a readable title (`WeCom · {peer} {MM-DD} {HH:mm:ss}`, derived from the minted directory name). - Session titles are topic-only now: the `userid:` / `chatid:` prefix is gone; caller identity is carried by the per-session workspace row. ### Changed - `/new` no longer disposes the previous session's agent: a disposed session left the host's live-session projection, blanking its sidebar row until a reload. The old conversation stays visible and resumable; teardown happens at pool shutdown. - Uploads land inside the conversation's own sandbox directory. ### Fixed - Restart re-grouping no longer mints empty workspace rows for legacy sessions: rows are created only when the session's stored cwd matches its per-session directory (anchored on the id tail). ## [0.1.24] - 2026-08-17 ### Added - Optional `provider` / `model` config pins one model route for every WeCom conversation (both fields must be set together; a half-set pair fails loudly at plugin startup). - Resumed WeCom conversations now inherit the model logged in their session header when no explicit route is configured — a model switched in the web UI stays in effect across restarts instead of reverting to the harness default. The pool installs a per-agent model selection (the harness's own mechanism), so a later web-UI switch still overrides everything. ## [0.1.23] - 2026-08-17 ### Changed - The sidebar status button now reads `Active: N Total: M` — running WeCom conversations and total WeCom sessions. Non-WeCom sessions no longer appear anywhere in the status UI: the floating panel's Sessions row shows only the WeCom count, and the live-agent list stays WeCom-only. ## [0.1.22] - 2026-08-17 ### Changed - The `[userid]:` sender label is now prepended to message content only in group chats. Single chats have exactly one sender, so the label is dropped and the bubble shows the message text alone. ## [0.1.21] - 2026-08-17 ### Changed - The per-conversation display peer (userid / chatid) is now persisted in `.dsh-wecom-state.json` next to the reset epochs, so the status panel shows the right peer immediately after a restart instead of falling back to `WeCom · single/group` until the next message. Legacy flat epoch state files migrate transparently on load. ## [0.1.20] - 2026-08-17 ### Changed - The status panel now lists WeCom conversations only (web sessions are filtered out), and each row's label is the conversation's chatid (groups) or userid (single chats) resolved host-side, instead of the generic `WeCom · single` / `WeCom · group` scope text. The peer is known once a message touched the conversation in this process; until then the row falls back to the scope label. ## [0.1.19] - 2026-08-17 ### Fixed - The status panel (`/api/wecom/status` and the sidebar popover) showed the same stale model for every live agent: it read the creation-time `agent.options` snapshot. It now reads each session's folded request header — the model the conversation actually runs on — and falls back to the creation options before the first request is logged. Per-session model switches in the web UI now show up correctly. ## [0.1.18] - 2026-08-17 ### Changed - WeCom session titles are now locked: the plugin watches every `session/title` event host-wide (so it covers sessions resumed outside the pool, e.g. the web UI or the rename API) and reverts manual renames back to the canonical title — the prefixed harness-generated LLM title, or a legacy session's existing title. Renames that already match the canonical title pass through. The enforcement is best-effort and never fails a turn. ## [0.1.17] - 2026-08-17 ### Changed - The sender metadata line is now a compact `[userid]:text` prefix instead of the long `[WeCom group message from WeCom user …]` bracket. The full-width colon is deliberate: an ASCII colon after a bracket would parse as a Markdown link reference and disappear from rendered chat bubbles. ### Fixed - Text messages no longer carry a spurious `[Unsupported WeCom message type]` note — the sender-label rewrite broke the emptiness check that guarded it. ### Docs - READMEs now document installing from npm (`dsh plugin --profile web add dsh-wecom`), including version pinning and upgrades. ## [0.1.15] - 2026-08-17 ### Changed - Group conversations now use the group `chatid` (falling back to the sender userid) as the session-title prefix, while single chats keep the sender userid — so the sidebar prefix always matches the conversation identity. - Message content no longer masks the sender userid: each message carries the full WeCom userid in its leading metadata line, so chat bubbles distinguish senders (the masked 8-character prefix is gone). ## [0.1.14] - 2026-08-17 ### Changed - Session titles: the plugin no longer renames a conversation to its first message text. WeCom sessions now keep the harness-generated short LLM title, prefixed with the first sender's WeCom userid (`userid:标题`), so the sidebar shows who each conversation belongs to. The prefix is applied only to harness-generated (provider) titles: manual renames in the web UI are left untouched, and the deterministic fallback is not pinned, so pending LLM title generation is never superseded. Title rewrites are best-effort and never fail the turn. ## [0.1.13] - 2026-08-17 ### Changed - Agent working directory (`cwd`) now defaults to `~/.wecom-sessions` instead of the process cwd, keeping WeCom sessions, uploads, and state files out of wherever `dsh web` was launched. The field is optional in the row config; resolution order is explicit `cwd` → `DSH_WECOM_CWD` environment override → `~/.wecom-sessions`, and a relative value rejects loudly. Existing persisted conversations keep their stored cwd (they stay under the old workspace until `/new` starts a fresh session in the new directory). ## [0.1.12] - 2026-08-17 ### Changed - Sidebar-foot WeCom button: the session summary (`6 WeCom · 29 total`) now sits right-aligned on the far side of the row instead of a pill hugging the "WeCom" label. ## [0.1.11] - 2026-08-17 ### Changed - Sidebar-foot WeCom button polish: slightly more line height/padding, and a live count pill next to the "WeCom" label showing the number of currently running WeCom conversations. ## [0.1.10] - 2026-08-17 ### Fixed - A turn no longer fails with `cannot create effect on inactive context` when the conversation's agent is disposed between being resolved and the turn subscribing to it (e.g. a `/new`/`/clear`, or the user closing the session in the web UI, while the message was still queued). The pool now re-resolves a live agent right before driving the turn. ## [0.1.9] - 2026-08-17 ### Added - `/clear` command: an alias of `/new` that drops the current conversation and starts a fresh one, so users get the expected "clear context" spelling alongside `/new`. ## [0.1.8] - 2026-08-17 ### Fixed - `/new` now survives a process restart: the per-conversation reset counter is persisted to `.dsh-wecom-state.json` in the agent cwd and reloaded on start. Previously the counter was in-memory only, so after a restart the next message resumed the ORIGINAL session with its full history instead of opening a fresh conversation. ## [0.1.7] - 2026-08-17 ### Fixed - A successful (re)authentication now clears the sticky `lastError` in the channel status: the sidebar panel no longer keeps showing a failure the channel already recovered from (e.g. a transient `write EPROTO` during a reconnect). The recovered message is logged before clearing, so the incident stays traceable. ## [0.1.6] - 2026-08-15 ### Changed - READMEs (EN/ZH) rewritten in the popular GitHub style: badges, quick-start-first layout, feature cards, and compact config/command tables; `showToolCalls` description updated to the 0.1.5 think-card rendering. ## [0.1.5] - 2026-08-15 ### Changed - Tool-call activity is now rendered as a compact list inside the native `` card (arguments previewed from their most informative field) instead of a markdown footer in the reply body, keeping the visible answer clean. ## [0.1.4] - 2026-08-15 > Superseded: the 0.1.4 npm tarball was published from a stale base that missed > the 0.1.2 changes; use 0.1.5 instead. ## [0.1.3] - 2026-08-15 ### Changed - Package metadata: added the `author` field (`TtTRz `) so npm surfaces the correct author on the registry page instead of the publishing account. ## [0.1.2] - 2026-08-15 ### Added - `/compact` command: summarizes the conversation's older history into one summary through the optional `ctx.compaction` seam, when that capability is mounted in the running harness. ### Changed - Bot commands renamed to the industry-standard set — `/ping`, `/help`, `/status`, `/stop`, `/new` — matching common Discord/Slack bot and AI assistant conventions. The old `/bot-*` names are removed. ## [0.1.1] - 2026-08-15 ### Added - Token-level streaming: model text and reasoning deltas are flushed to WeCom as they are produced (`streaming`, default on; `streamFlushMs` controls the cadence), instead of a single "Working…" ack + final answer. - Native thinking card: reasoning rides inside a `` block that stays open while the model thinks and closes when the answer starts, which the WeCom client renders as its collapsible "思考过程" card (`showReasoning`, default on). Nested/foreign think tags are stripped so exactly one block is sent. - Optional tool-call activity list on the final reply (`showToolCalls`, default on), budgeted under the WeCom reply byte cap. - Runtime overview in `GET /api/wecom/status` and the sidebar panel: live agents (running first, WeCom-flagged, with model), session counts (total and WeCom), and process/machine load (RSS, uptime, load average). ### Fixed - A WeCom message no longer fails with `cannot prepare session ... while it is live` when the conversation's session is already open elsewhere (e.g. the user opened it in the web UI, which resumes the persisted session). The pool now adopts the live agent instead of resuming it a second time, and re-opens the conversation when an agent it tracked was disposed by its owner. - Continuing a chat after its session was archived in the web UI no longer keeps the conversation invisible forever: the harness has no unarchive API, so the pool skips archived session ids and starts a fresh, visible session for new WeCom activity. - Restart loop (`runChannelLoop`): the channel restarts itself after every unrecoverable end — auth failure, reconnect exhaustion, or replacement by another client — after `restartIntervalMs` (default 10s), instead of leaving a failed fiber and a dead bot. - Workspace grouping: the pool claims a workspace on `cwd` (title `workspaceTitle`, default `WeCom`) and adds every WeCom conversation session to it, so chats stop landing in the sidebar's "Ungrouped" bucket. - Host-wide `wecomChannelStatus` service exposing a scalar-only `snapshot()` (`connected`, `stopping`, `conversations`, `authenticatedAt`, `lastError`). - `GET /api/wecom/status` route (registered only when a web server is present) serving the JSON status payload for dashboards and the bundled browser UI. - Browser client half (`dsh.client`, served as `/plugins/dsh-wecom/client.js`): a WeCom status action with a connection dot in the sidebar foot and a floating status panel over `shell.overlay`, polling the status route. - Initial WeCom AI Bot long-connection channel plugin for DeepSeek Harness. - Per-conversation persistent agents with a mounted `preset` (default `standard`). - Persistent `systemPrompt.section` instructions layered on the persona. - Single-chat and group text handling plus voice transcription. - Inbound images: download + AES decryption via the official SDK, durable Harness attachments, and a text fallback when the model cannot view images (`imageMode`: `auto` / `always` / `never`). - Inbound files and videos: downloads land in the agent's workspace (`.wecom-uploads/`) and are referenced by path for the agent's own tools. - Access policies (`open` / `allowlist` / `disabled`) for single and group scopes. - Bot commands: `/bot-ping`, `/bot-help`, `/bot-status`, `/bot-cancel`, `/bot-new`. - Response timeout that cancels the in-flight turn. - Global concurrency cap (`maxConcurrent`). - Streaming reply ack + final answer. - Secret resolution via `ctx.credentials`. - Packaging: `prepare` script added so git installs build from source; `package-lock.json` re-synced with `package.json` (was missing four transitive deps, breaking `npm ci`). - Docs: install section documents the pnpm ≥10 `allowBuilds` authorization for git installs and the prebuilt-tarball alternative.