@newbase-clawchat/openclaw-clawchat 2026.4.15 → 2026.4.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newbase-clawchat/openclaw-clawchat",
3
- "version": "2026.4.15",
3
+ "version": "2026.4.20",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "files": [
6
6
  "index.ts",
@@ -58,17 +58,20 @@ type StreamingReplyHooks = {
58
58
  * Reply dispatcher for openclaw-clawchat.
59
59
  *
60
60
  * Streaming mode (`account.replyMode === "stream"`, no replyCtx):
61
- * 1. `onReplyStart` opens a buffered streaming session `message.created`
62
- * fires immediately.
63
- * 2. As the agent runs, `onPartialReply` / `onReasoningStream` snapshots
64
- * and `deliver(block|tool)` deltas feed into the session; each flush
65
- * emits `message.add` with the new text delta (chunked by
61
+ * 1. `onReplyStart` just resets accumulators. It does NOT open the session
62
+ * yet — that would leave a ghost `message.created + done` bubble if the
63
+ * run produces nothing (e.g., agent not configured, send-policy denied).
64
+ * 2. On first real content (deliver block/tool or partial/reasoning
65
+ * snapshot), `queueStreamSnapshot` lazily opens the session, which emits
66
+ * `message.created` plus the first `message.add`.
67
+ * 3. Subsequent snapshots/deltas emit `message.add` frames (chunked by
66
68
  * `stream.flushIntervalMs` + `stream.minChunkChars`).
67
- * 3. On run end (`onIdle`), the session flushes pending buffer, emits
69
+ * 4. On run end (`onIdle`), the session flushes pending buffer, emits
68
70
  * `message.done`, then the merged full text plus any accumulated
69
71
  * media is emitted as a separate `message.send` / `message.reply` —
70
72
  * mirroring the clawling-channel v1 pattern where streaming `agent`
71
- * events are followed by a consolidated `chat` final.
73
+ * events are followed by a consolidated `chat` final. Empty runs emit
74
+ * nothing at all (no created/done/reply) — they only log a skip line.
72
75
  *
73
76
  * Static mode or replyCtx: bypass streaming and emit one `message.send` /
74
77
  * `message.reply` per deliver with text + media.
@@ -133,10 +136,14 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
133
136
  const openSessionIfNeeded = () => {
134
137
  if (!streamingEnabled || streamingSession || streamCreatedEmitted) return;
135
138
  streamCreatedEmitted = true;
136
- // Use the inbound message_id when available so the streaming frames
137
- // correlate with the user message that triggered this reply; fall back
138
- // to a generated id only when not provided (e.g. tests).
139
- streamingMessageId = inboundMessageId ?? `${account.userId}-${Date.now()}`;
139
+ // Mint a fresh agent-side message_id at `message.created` time. All
140
+ // subsequent `message.add` / `message.done` / `message.reply` frames for
141
+ // this stream reuse it. Once the stream finalizes (done or reply), this
142
+ // id is retired — the next inbound turn spawns a new dispatcher instance
143
+ // which mints its own id. The inbound user message_id lives on
144
+ // `replyTo.msgId`; keeping the two distinct avoids the agent's reply
145
+ // frames shadowing the user turn they answer.
146
+ streamingMessageId = `${account.userId}-stream-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
140
147
  streamingSession = openBufferedStreamingSession({
141
148
  client,
142
149
  routing,
@@ -269,6 +276,12 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
269
276
  // If `onReplyStart` fires again during the same dispatcher instance
270
277
  // (e.g. typing controller re-entry), we must NOT tear down the stream
271
278
  // session — that would cause a second `message.created`.
279
+ //
280
+ // We deliberately do NOT open the streaming session here. Opening it
281
+ // eagerly would emit `message.created` even for runs that ultimately
282
+ // produce nothing (unknown agent, send-policy denied, etc.), leaving a
283
+ // ghost empty bubble. The session opens lazily via queueStreamSnapshot
284
+ // on the first real content.
272
285
  if (!streamCreatedEmitted) {
273
286
  streamText = "";
274
287
  reasoningText = "";
@@ -277,7 +290,6 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
277
290
  streamingClosed = false;
278
291
  runDone = false;
279
292
  }
280
- if (streamingEnabled) openSessionIfNeeded();
281
293
  },
282
294
  deliver: async (payload: ReplyPayload, info?: { kind: "tool" | "block" | "final" }) => {
283
295
  const text = payload.text ?? "";