# dsh-send-debounce A send buffer for DSH chat: when you type several short messages in a row, they no longer trigger N separate model wake-ups and replies. After you stop for a few seconds, the batch is **merged into a single turn**, so the model answers once. > 中文说明见 [README.md](./README.md). / Chinese: see [README.md](./README.md). ## What it does - Interception point: each agent's server-side `followup()` (`@deepseek-ai/dsh-agent-loop`). - A user message is queued **immediately** (visible in the web UI) but does **not** wake the model yet. - The silence window defaults to **5 seconds**; every new short message resets the timer. - When the timer fires, the batch is merged into one message and wakes the model once. - Long messages (≥120 characters by default) and messages with images/attachments are sent **immediately**. - `steer()` (interjections) and plugin-sourced `agent.followup` calls (e.g. `dsh-proactive`, `dsh-tick`) are left untouched. ## Configuration (environment variables; restart dsh to apply) | Variable | Default | Meaning | | --- | --- | --- | | `DSH_SEND_DEBOUNCE` | on | `off` / `0` / `false` / `no` disables the plugin | | `DSH_SEND_DEBOUNCE_MS` | `5000` | silence window in ms; `0` disables the delay; invalid values fall back | | `DSH_SEND_DEBOUNCE_MAX_CHARS` | `120` | "short message" limit; `0` = delay everything; invalid values fall back | ## Install From npm (public package): ```sh dsh plugin --profile web add dsh-send-debounce ``` From a local directory (development / self-hosted): ```sh dsh plugin --profile web add file:/path/to/dsh-send-debounce ``` Then add `dsh-send-debounce` to that profile's `dsh.profile.bundles` (that entry is the `cordis.patch.yml` layer), restart, and confirm the web UI is still up: ```sh dsh --profile web --dump-config | grep -n dsh-send-debounce sudo systemctl restart dsh curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3080/ ``` ## Safety boundaries The plugin buffers and does nothing else: - **Only handles messages the user typed in the web UI** (`message.source.kind === "user"`). Plugin-sourced messages (`dsh-proactive`, `dsh-tick`, ...) pass through unchanged, otherwise their `whenIdle()` would stall. - **Does not touch `steer()`** and does not rewrite session history. It only merges the batch it queued itself and hands it to the original `followup()`. - **Long messages and messages with images/attachments are sent immediately** — never delayed. - **The original text is never rewritten**: merging only inserts a blank line between messages and keeps each message's text and order. - **Any error falls back to native behavior**: a failed inbox read, merge, or send is caught and either replayed message-by-message or passed straight through, so the chat never hangs. - **No network, no workspace/file access, no environment secrets**: it reads only the three `DSH_SEND_DEBOUNCE*` variables. The whole implementation is `index.mjs` — read it yourself. ## Known limitations - **The silence timer lives in the process.** If dsh restarts before the flush, the messages are still persisted in the session, but they are **not woken automatically** — they wait for the next message (or a manual trigger). Give it a few seconds to flush before restarting. - **Environment variable changes require a dsh restart**; no hot reload. - **Short/long is decided by character count**, not meaning: a short but urgent line still waits. - **The model sees the merged text**, not the original number of turns. The raw enqueue records remain in the session's inbox events, which is useful for debugging only. - Server-side only (mobile browsers benefit too); it does not change the UI. ## Rollback Restore the profile backup: ```sh cp ~/.dsh/profiles/web/package.json.bak- ~/.dsh/profiles/web/package.json sudo systemctl restart dsh ``` Or simply remove `dsh-send-debounce` from `dsh.profile.bundles` and restart. Uninstalling clears the timer and restores the original `followup()`. ## Tests No dsh environment needed — plain Node: ```sh npm test # same as: node test.mjs ``` Covered: burst merge, immediate long messages, plugin-source pass-through, image pass-through, separate turns when the gap exceeds the window, uninstall restore, disabled switch, config parsing with invalid-value fallback, repeat install/uninstall, timer cleanup on agent disposal, and the pure helpers. ## License MIT, see [LICENSE](./LICENSE).