@alfe.ai/openclaw-chat 0.3.3 → 0.4.0

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/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as plugin, c as AlfeChannelConfig, l as AlfePluginConfig, s as createAlfeChannelPlugin, u as AlfeResolvedAccount } from "./plugin.cjs";
1
+ import { d as AlfePluginConfig, f as AlfeResolvedAccount, l as createAlfeChannelPlugin, o as plugin, u as AlfeChannelConfig } from "./plugin.cjs";
2
2
 
3
3
  //#region src/session-store.d.ts
4
4
 
@@ -19,6 +19,35 @@ interface ChatMessage {
19
19
  senderId?: string;
20
20
  senderName?: string;
21
21
  }
22
+ /**
23
+ * Persisted form of a turn's agent activity (tool cards + thinking), so a
24
+ * refresh can replay what the live `chat-activity` stream showed. Terminal
25
+ * by construction: no `progressText` (transient streaming window) and no
26
+ * `running` status — a tool still running at flush time is persisted as
27
+ * `interrupted`. Tool text fields arrive pre-capped (activity-serialize.ts);
28
+ * thinking text is capped at append time (MAX_ACTIVITY_THINKING_CHARS).
29
+ */
30
+ type ChatActivityRecord = {
31
+ kind: 'thinking';
32
+ text: string;
33
+ ts: number;
34
+ } | {
35
+ kind: 'tool';
36
+ toolCallId: string;
37
+ name: string;
38
+ status: 'done' | 'failed' | 'interrupted';
39
+ summary?: string;
40
+ argsText?: string;
41
+ resultText?: string;
42
+ isError?: boolean;
43
+ durationMs?: number;
44
+ truncated?: {
45
+ args?: boolean;
46
+ progress?: boolean;
47
+ result?: boolean;
48
+ };
49
+ ts: number;
50
+ };
22
51
  interface SessionData {
23
52
  sessionId: string;
24
53
  agentId: string;
@@ -28,6 +57,8 @@ interface SessionData {
28
57
  createdAt: string;
29
58
  updatedAt: string;
30
59
  messages: ChatMessage[];
60
+ /** Absent on session files written before activity persistence shipped. */
61
+ activity?: ChatActivityRecord[];
31
62
  }
32
63
  interface SessionSummary {
33
64
  sessionId: string;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as plugin, c as AlfeChannelConfig, l as AlfePluginConfig, s as createAlfeChannelPlugin, u as AlfeResolvedAccount } from "./plugin.js";
1
+ import { d as AlfePluginConfig, f as AlfeResolvedAccount, l as createAlfeChannelPlugin, o as plugin, u as AlfeChannelConfig } from "./plugin.js";
2
2
 
3
3
  //#region src/session-store.d.ts
4
4
 
@@ -19,6 +19,35 @@ interface ChatMessage {
19
19
  senderId?: string;
20
20
  senderName?: string;
21
21
  }
22
+ /**
23
+ * Persisted form of a turn's agent activity (tool cards + thinking), so a
24
+ * refresh can replay what the live `chat-activity` stream showed. Terminal
25
+ * by construction: no `progressText` (transient streaming window) and no
26
+ * `running` status — a tool still running at flush time is persisted as
27
+ * `interrupted`. Tool text fields arrive pre-capped (activity-serialize.ts);
28
+ * thinking text is capped at append time (MAX_ACTIVITY_THINKING_CHARS).
29
+ */
30
+ type ChatActivityRecord = {
31
+ kind: 'thinking';
32
+ text: string;
33
+ ts: number;
34
+ } | {
35
+ kind: 'tool';
36
+ toolCallId: string;
37
+ name: string;
38
+ status: 'done' | 'failed' | 'interrupted';
39
+ summary?: string;
40
+ argsText?: string;
41
+ resultText?: string;
42
+ isError?: boolean;
43
+ durationMs?: number;
44
+ truncated?: {
45
+ args?: boolean;
46
+ progress?: boolean;
47
+ result?: boolean;
48
+ };
49
+ ts: number;
50
+ };
22
51
  interface SessionData {
23
52
  sessionId: string;
24
53
  agentId: string;
@@ -28,6 +57,8 @@ interface SessionData {
28
57
  createdAt: string;
29
58
  updatedAt: string;
30
59
  messages: ChatMessage[];
60
+ /** Absent on session files written before activity persistence shipped. */
61
+ activity?: ChatActivityRecord[];
31
62
  }
32
63
  interface SessionSummary {
33
64
  sessionId: string;
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { i as plugin, o as createAlfeChannelPlugin } from "./plugin2.js";
1
+ import { a as plugin, c as createAlfeChannelPlugin } from "./plugin2.js";
2
2
  export { createAlfeChannelPlugin, plugin as default };
package/dist/plugin.cjs CHANGED
@@ -7,4 +7,6 @@ exports.admitAgentEvent = require_plugin.admitAgentEvent;
7
7
  exports.buildA2ACompletePayload = require_plugin.buildA2ACompletePayload;
8
8
  exports.buildToolActivity = require_plugin.buildToolActivity;
9
9
  exports.default = require_plugin.plugin;
10
+ exports.joinAssistantText = require_plugin.joinAssistantText;
11
+ exports.rewriteAssistantText = require_plugin.rewriteAssistantText;
10
12
  exports.stripLeakedToolSummary = require_plugin.stripLeakedToolSummary;
package/dist/plugin.d.cts CHANGED
@@ -334,6 +334,21 @@ type ChatActivity = {
334
334
  ts?: number;
335
335
  seq?: number;
336
336
  };
337
+ /**
338
+ * Join the text parts of an assistant-stream event's content array.
339
+ * Assistant `evt.data` is a message object whose `content` is an array of
340
+ * typed parts; deltas are CUMULATIVE snapshots (the relay length-compares
341
+ * them — see adapter.ts's lastSentLength slicing). Non-array/absent content
342
+ * yields '' (nothing to divert).
343
+ */
344
+ declare function joinAssistantText(data: Record<string, unknown>): string;
345
+ /**
346
+ * Rebuild an assistant event's message with its text parts replaced by one
347
+ * `{type:'text', text: visible}` part. Non-text parts are preserved — the
348
+ * relay's consumers (adapter/inject/voice-bridge) only join text parts, so
349
+ * relative ordering against non-text items is not load-bearing.
350
+ */
351
+ declare function rewriteAssistantText(data: Record<string, unknown>, visible: string): Record<string, unknown>;
337
352
  /**
338
353
  * Extract structured tool status from an OpenClaw `tool`-stream event.
339
354
  * We send the emoji-free raw `name` plus a lifecycle `status` and let the
@@ -396,4 +411,4 @@ declare const plugin: {
396
411
  deactivate(api: PluginApi): Promise<void>;
397
412
  };
398
413
  //#endregion
399
- export { plugin as a, AlfeChannelConfig as c, buildToolActivity as i, AlfePluginConfig as l, admitAgentEvent as n, stripLeakedToolSummary as o, buildA2ACompletePayload as r, createAlfeChannelPlugin as s, RunEventGate as t, AlfeResolvedAccount as u };
414
+ export { joinAssistantText as a, stripLeakedToolSummary as c, AlfePluginConfig as d, AlfeResolvedAccount as f, buildToolActivity as i, createAlfeChannelPlugin as l, admitAgentEvent as n, plugin as o, buildA2ACompletePayload as r, rewriteAssistantText as s, RunEventGate as t, AlfeChannelConfig as u };
package/dist/plugin.d.ts CHANGED
@@ -334,6 +334,21 @@ type ChatActivity = {
334
334
  ts?: number;
335
335
  seq?: number;
336
336
  };
337
+ /**
338
+ * Join the text parts of an assistant-stream event's content array.
339
+ * Assistant `evt.data` is a message object whose `content` is an array of
340
+ * typed parts; deltas are CUMULATIVE snapshots (the relay length-compares
341
+ * them — see adapter.ts's lastSentLength slicing). Non-array/absent content
342
+ * yields '' (nothing to divert).
343
+ */
344
+ declare function joinAssistantText(data: Record<string, unknown>): string;
345
+ /**
346
+ * Rebuild an assistant event's message with its text parts replaced by one
347
+ * `{type:'text', text: visible}` part. Non-text parts are preserved — the
348
+ * relay's consumers (adapter/inject/voice-bridge) only join text parts, so
349
+ * relative ordering against non-text items is not load-bearing.
350
+ */
351
+ declare function rewriteAssistantText(data: Record<string, unknown>, visible: string): Record<string, unknown>;
337
352
  /**
338
353
  * Extract structured tool status from an OpenClaw `tool`-stream event.
339
354
  * We send the emoji-free raw `name` plus a lifecycle `status` and let the
@@ -396,4 +411,4 @@ declare const plugin: {
396
411
  deactivate(api: PluginApi): Promise<void>;
397
412
  };
398
413
  //#endregion
399
- export { plugin as a, AlfeChannelConfig as c, buildToolActivity as i, AlfePluginConfig as l, admitAgentEvent as n, stripLeakedToolSummary as o, buildA2ACompletePayload as r, createAlfeChannelPlugin as s, RunEventGate as t, AlfeResolvedAccount as u };
414
+ export { joinAssistantText as a, stripLeakedToolSummary as c, AlfePluginConfig as d, AlfeResolvedAccount as f, buildToolActivity as i, createAlfeChannelPlugin as l, admitAgentEvent as n, plugin as o, buildA2ACompletePayload as r, rewriteAssistantText as s, RunEventGate as t, AlfeChannelConfig as u };
package/dist/plugin.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as stripLeakedToolSummary, i as plugin, n as buildA2ACompletePayload, r as buildToolActivity, t as admitAgentEvent } from "./plugin2.js";
2
- export { admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, stripLeakedToolSummary };
1
+ import { a as plugin, i as joinAssistantText, n as buildA2ACompletePayload, o as rewriteAssistantText, r as buildToolActivity, s as stripLeakedToolSummary, t as admitAgentEvent } from "./plugin2.js";
2
+ export { admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, joinAssistantText, rewriteAssistantText, stripLeakedToolSummary };
package/dist/plugin2.cjs CHANGED
@@ -344,6 +344,31 @@ async function addMessage(sessionId, role, content, senderId, senderName) {
344
344
  writeLocks.set(sessionId, next);
345
345
  await next;
346
346
  }
347
+ const MAX_ACTIVITY_PER_SESSION = 500;
348
+ const MAX_ACTIVITY_THINKING_CHARS = 16e3;
349
+ /**
350
+ * Append a turn's terminalized activity records. Called ONCE per turn (from
351
+ * the dispatch `finally`) — never per-delta: thinking deltas are
352
+ * high-frequency and each save rewrites the whole session file.
353
+ */
354
+ async function appendActivity(sessionId, entries) {
355
+ if (entries.length === 0) return;
356
+ const next = (writeLocks.get(sessionId) ?? Promise.resolve()).then(async () => {
357
+ const session = await getSession(sessionId);
358
+ if (!session) return;
359
+ const capped = entries.map((e) => e.kind === "thinking" && e.text.length > MAX_ACTIVITY_THINKING_CHARS ? {
360
+ ...e,
361
+ text: e.text.slice(0, MAX_ACTIVITY_THINKING_CHARS)
362
+ } : e);
363
+ const combined = [...session.activity ?? [], ...capped];
364
+ session.activity = combined.length > MAX_ACTIVITY_PER_SESSION ? combined.slice(combined.length - MAX_ACTIVITY_PER_SESSION) : combined;
365
+ await saveSession(session);
366
+ }).finally(() => {
367
+ if (writeLocks.get(sessionId) === next) writeLocks.delete(sessionId);
368
+ });
369
+ writeLocks.set(sessionId, next);
370
+ await next;
371
+ }
347
372
  async function listSessions(filters, limit = 50) {
348
373
  await ensureDir();
349
374
  let files;
@@ -848,6 +873,101 @@ function extractResultText(result) {
848
873
  return parts.join("\n");
849
874
  }
850
875
  //#endregion
876
+ //#region src/think-tags.ts
877
+ /**
878
+ * Inline `<think>` span handling for assistant text.
879
+ *
880
+ * Reasoning models routed through the AI proxy (DeepSeek `deepseek-*`,
881
+ * Zhipu `glm-*`, …) emit their reasoning INLINE as `<think>…</think>` inside
882
+ * the normal content stream instead of a structured thinking channel. The
883
+ * openclaw-chat plugin (0.4.0+) diverts those spans into thinking activity
884
+ * frames at the source; this relay-side copy is the DEFENSIVE layer for
885
+ * agents still running older plugin versions — leaked reasoning is dropped
886
+ * here, never synthesized into activity (that's the plugin's job).
887
+ *
888
+ * KEEP IN SYNC (byte-identical): this module exists at BOTH
889
+ * `services/chat/src/lib/think-tags.ts` (relay, defensive layer)
890
+ * `packages/openclaw-chat/src/think-tags.ts` (plugin, at-source divert)
891
+ * The published plugin cannot import service internals, so the module is
892
+ * duplicated, same as the ChatActivity shape.
893
+ *
894
+ * Known accepted limitation: a literal `<think>` in legitimate prose or a
895
+ * code fence is treated as reasoning and stripped/diverted.
896
+ */
897
+ const OPEN_TAG = "<think>";
898
+ const CLOSE_TAG = "</think>";
899
+ /**
900
+ * Split CUMULATIVE assistant text into the user-visible part and the
901
+ * reasoning part.
902
+ *
903
+ * Invariant (load-bearing): for successive cumulative snapshots that grow by
904
+ * appending, `visible` is monotonically append-only — `visible(n)` is always
905
+ * a prefix of `visible(n + 1)`. The delta relays slice by a `lastSentLength`
906
+ * offset into this string; if `visible` ever shrank or rewrote earlier
907
+ * characters, clients would receive corrupted deltas. This holds because the
908
+ * scan is deterministic left-to-right and a trailing PARTIAL tag (any strict
909
+ * prefix of the next expected tag, e.g. `"<thi"` or `"</th"`) is HELD BACK
910
+ * from both outputs until later text disambiguates it.
911
+ */
912
+ function splitThinkTags(cumulative) {
913
+ let visible = "";
914
+ let thinking = "";
915
+ let inThink = false;
916
+ let i = 0;
917
+ const n = cumulative.length;
918
+ while (i < n) {
919
+ const tag = inThink ? CLOSE_TAG : OPEN_TAG;
920
+ const idx = cumulative.indexOf(tag, i);
921
+ if (idx !== -1) {
922
+ const chunk = cumulative.slice(i, idx);
923
+ if (inThink) thinking += chunk;
924
+ else visible += chunk;
925
+ inThink = !inThink;
926
+ i = idx + tag.length;
927
+ continue;
928
+ }
929
+ let rest = cumulative.slice(i);
930
+ const held = trailingPartialTagLength(rest, tag);
931
+ if (held > 0) rest = rest.slice(0, rest.length - held);
932
+ if (inThink) thinking += rest;
933
+ else visible += rest;
934
+ break;
935
+ }
936
+ return {
937
+ visible,
938
+ thinking
939
+ };
940
+ }
941
+ /** Length of the longest strict prefix of `tag` that is a suffix of `text`. */
942
+ function trailingPartialTagLength(text, tag) {
943
+ const max = Math.min(text.length, tag.length - 1);
944
+ for (let len = max; len > 0; len--) if (text.endsWith(tag.slice(0, len))) return len;
945
+ return 0;
946
+ }
947
+ /**
948
+ * Strip `<think>…</think>` spans (and an unclosed trailing `<think>…`) from
949
+ * FINAL text, then tidy the whitespace the spans leave behind.
950
+ *
951
+ * Idempotent — `stripThinkSpans(stripThinkSpans(x)) === stripThinkSpans(x)`.
952
+ * New plugin + new relay both strip, so the second pass must be a no-op; the
953
+ * fixpoint loop also covers pathological inputs where removing one span
954
+ * makes the surrounding fragments form a new one.
955
+ *
956
+ * NOT for sliced/streaming paths — the trim + blank-line collapse breaks
957
+ * prefix alignment with `lastSentLength`; those paths use
958
+ * `splitThinkTags(...).visible` instead.
959
+ */
960
+ function stripThinkSpans(text) {
961
+ let out = text;
962
+ let prev;
963
+ do {
964
+ prev = out;
965
+ out = out.replace(/<think>[\s\S]*?<\/think>/g, "");
966
+ } while (out !== prev);
967
+ out = out.replace(/<think>[\s\S]*$/, "");
968
+ return out.replace(/\n{3,}/g, "\n\n").trim();
969
+ }
970
+ //#endregion
851
971
  //#region src/plugin.ts
852
972
  /**
853
973
  * @alfe.ai/openclaw-chat — OpenClaw chat channel plugin.
@@ -913,7 +1033,7 @@ function asString(v) {
913
1033
  * Returns null when there's nothing to forward.
914
1034
  */
915
1035
  function buildThinkingActivity(data) {
916
- const text = asString(data.delta) ?? asString(data.text) ?? asString(data.content) ?? asString(data.message?.text);
1036
+ const text = asString(data.delta) ?? asString(data.text) ?? asString(data.content) ?? asString(data.reasoning) ?? asString(data.reasoning_content) ?? asString(data.message?.text);
917
1037
  if (!text) return null;
918
1038
  return {
919
1039
  kind: "thinking",
@@ -922,6 +1042,34 @@ function buildThinkingActivity(data) {
922
1042
  };
923
1043
  }
924
1044
  /**
1045
+ * Join the text parts of an assistant-stream event's content array.
1046
+ * Assistant `evt.data` is a message object whose `content` is an array of
1047
+ * typed parts; deltas are CUMULATIVE snapshots (the relay length-compares
1048
+ * them — see adapter.ts's lastSentLength slicing). Non-array/absent content
1049
+ * yields '' (nothing to divert).
1050
+ */
1051
+ function joinAssistantText(data) {
1052
+ const content = data.content;
1053
+ if (!Array.isArray(content)) return "";
1054
+ return content.filter((c) => typeof c === "object" && c !== null && c.type === "text" && typeof c.text === "string").map((c) => c.text).join("");
1055
+ }
1056
+ /**
1057
+ * Rebuild an assistant event's message with its text parts replaced by one
1058
+ * `{type:'text', text: visible}` part. Non-text parts are preserved — the
1059
+ * relay's consumers (adapter/inject/voice-bridge) only join text parts, so
1060
+ * relative ordering against non-text items is not load-bearing.
1061
+ */
1062
+ function rewriteAssistantText(data, visible) {
1063
+ const nonText = (Array.isArray(data.content) ? data.content : []).filter((c) => !(typeof c === "object" && c !== null && c.type === "text"));
1064
+ return {
1065
+ ...data,
1066
+ content: [...nonText, {
1067
+ type: "text",
1068
+ text: visible
1069
+ }]
1070
+ };
1071
+ }
1072
+ /**
925
1073
  * Extract structured tool status from an OpenClaw `tool`-stream event.
926
1074
  * We send the emoji-free raw `name` plus a lifecycle `status` and let the
927
1075
  * web render the `🛠️ <Label>: <summary> ✓/✗` row — never OpenClaw's raw
@@ -1171,6 +1319,43 @@ async function handleAgentRequest(request, log) {
1171
1319
  runId: null,
1172
1320
  sessionKey: null
1173
1321
  };
1322
+ let lastThinkingLen = 0;
1323
+ const turnThinking = {
1324
+ text: "",
1325
+ ts: 0
1326
+ };
1327
+ const turnTools = /* @__PURE__ */ new Map();
1328
+ const recordThinking = (text, ts) => {
1329
+ if (!turnThinking.text) turnThinking.ts = ts;
1330
+ turnThinking.text += text;
1331
+ };
1332
+ const recordTool = (activity, ts) => {
1333
+ const prev = turnTools.get(activity.toolCallId);
1334
+ turnTools.set(activity.toolCallId, {
1335
+ kind: "tool",
1336
+ toolCallId: activity.toolCallId,
1337
+ name: activity.name,
1338
+ status: activity.status === "running" ? prev?.status ?? "interrupted" : activity.status,
1339
+ ...activity.summary ?? prev?.summary ? { summary: activity.summary ?? prev?.summary } : {},
1340
+ ...prev?.argsText ?? activity.argsText ? { argsText: prev?.argsText ?? activity.argsText } : {},
1341
+ ...activity.resultText ?? prev?.resultText ? { resultText: activity.resultText ?? prev?.resultText } : {},
1342
+ ...(activity.isError ?? prev?.isError) !== void 0 ? { isError: activity.isError ?? prev?.isError } : {},
1343
+ ...(activity.durationMs ?? prev?.durationMs) !== void 0 ? { durationMs: activity.durationMs ?? prev?.durationMs } : {},
1344
+ ...activity.truncated ?? prev?.truncated ? { truncated: activity.truncated ?? prev?.truncated } : {},
1345
+ ts: prev?.ts ?? ts
1346
+ });
1347
+ };
1348
+ const flushTurnActivity = async () => {
1349
+ const entries = [];
1350
+ if (turnThinking.text) entries.push({
1351
+ kind: "thinking",
1352
+ text: turnThinking.text,
1353
+ ts: turnThinking.ts
1354
+ });
1355
+ for (const tool of turnTools.values()) entries.push(tool);
1356
+ entries.sort((a, b) => a.ts - b.ts);
1357
+ await appendActivity(sessionId, entries);
1358
+ };
1174
1359
  const UPDATE_THROTTLE_MS = 250;
1175
1360
  const pendingUpdates = /* @__PURE__ */ new Map();
1176
1361
  const flushToolUpdate = (toolCallId) => {
@@ -1187,33 +1372,58 @@ async function handleAgentRequest(request, log) {
1187
1372
  const unsubscribe = runtime.events.onAgentEvent((evt) => {
1188
1373
  if (!admitAgentEvent(runGate, evt)) return;
1189
1374
  if (evt.stream === "assistant") {
1375
+ const raw = joinAssistantText(evt.data);
1376
+ const { visible, thinking } = splitThinkTags(raw);
1377
+ if (thinking.length > lastThinkingLen) {
1378
+ const thinkingDelta = thinking.slice(lastThinkingLen);
1379
+ chatClient?.sendEvent("chat-activity", {
1380
+ runId: evt.runId,
1381
+ sessionKey: legacySessionKey,
1382
+ seq: evt.seq,
1383
+ state: "activity",
1384
+ activity: {
1385
+ kind: "thinking",
1386
+ text: thinkingDelta,
1387
+ delta: true,
1388
+ ts: evt.ts,
1389
+ seq: evt.seq
1390
+ }
1391
+ });
1392
+ recordThinking(thinkingDelta, evt.ts);
1393
+ lastThinkingLen = thinking.length;
1394
+ }
1395
+ const message = visible === raw && thinking.length === 0 ? evt.data : rewriteAssistantText(evt.data, visible);
1190
1396
  chatClient?.sendEvent("chat", {
1191
1397
  runId: evt.runId,
1192
1398
  sessionKey: legacySessionKey,
1193
1399
  seq: evt.seq,
1194
1400
  state: "delta",
1195
- message: evt.data
1401
+ message
1196
1402
  });
1197
1403
  return;
1198
1404
  }
1199
1405
  if (evt.stream === "thinking") {
1200
1406
  const activity = buildThinkingActivity(evt.data);
1201
- if (activity) chatClient?.sendEvent("chat-activity", {
1202
- runId: evt.runId,
1203
- sessionKey: legacySessionKey,
1204
- seq: evt.seq,
1205
- state: "activity",
1206
- activity: {
1207
- ...activity,
1208
- ts: evt.ts,
1209
- seq: evt.seq
1210
- }
1211
- });
1407
+ if (activity) {
1408
+ chatClient?.sendEvent("chat-activity", {
1409
+ runId: evt.runId,
1410
+ sessionKey: legacySessionKey,
1411
+ seq: evt.seq,
1412
+ state: "activity",
1413
+ activity: {
1414
+ ...activity,
1415
+ ts: evt.ts,
1416
+ seq: evt.seq
1417
+ }
1418
+ });
1419
+ if (activity.kind === "thinking") recordThinking(activity.text, evt.ts);
1420
+ }
1212
1421
  return;
1213
1422
  }
1214
1423
  if (evt.stream === "tool") {
1215
1424
  const activity = buildToolActivity(evt.data);
1216
1425
  if (activity?.kind !== "tool") return;
1426
+ recordTool(activity, evt.ts);
1217
1427
  const toolCallId = activity.toolCallId;
1218
1428
  const send = () => {
1219
1429
  chatClient?.sendEvent("chat-activity", {
@@ -1309,10 +1519,10 @@ async function handleAgentRequest(request, log) {
1309
1519
  } : {}
1310
1520
  },
1311
1521
  deliver: async (payload) => {
1312
- const responseText = stripLeakedToolSummary(payload.text ?? "");
1522
+ const responseText = stripLeakedToolSummary(stripThinkSpans(payload.text ?? ""));
1313
1523
  const mediaUrls = [...payload.mediaUrl ? [payload.mediaUrl] : [], ...payload.mediaUrls ?? []];
1314
1524
  await addMessage(sessionId, "assistant", responseText);
1315
- if (isA2A) a2aResponseBuffer += responseText;
1525
+ if (isA2A) a2aResponseBuffer += (a2aResponseBuffer && responseText ? "\n\n" : "") + responseText;
1316
1526
  chatClient?.notify("agent-message", {
1317
1527
  conversationId: conversationId ?? legacySessionKey,
1318
1528
  text: responseText,
@@ -1346,6 +1556,11 @@ async function handleAgentRequest(request, log) {
1346
1556
  } finally {
1347
1557
  unsubscribe();
1348
1558
  clearAllToolUpdates();
1559
+ try {
1560
+ await flushTurnActivity();
1561
+ } catch (err) {
1562
+ log.error(`Activity persistence failed: ${err instanceof Error ? err.message : String(err)}`);
1563
+ }
1349
1564
  if (isA2A) disarmA2AEndSignal();
1350
1565
  }
1351
1566
  }
@@ -1371,14 +1586,16 @@ async function handleSessionsGet(request, log) {
1371
1586
  if (!session) {
1372
1587
  chatClient?.sendResponse(request.id, true, {
1373
1588
  ok: false,
1374
- error: "Session not found"
1589
+ error: "Session not found",
1590
+ messages: []
1375
1591
  });
1376
1592
  return;
1377
1593
  }
1378
1594
  if (params.userId && session.userId && session.userId !== params.userId) {
1379
1595
  chatClient?.sendResponse(request.id, true, {
1380
1596
  ok: false,
1381
- error: "Session not found"
1597
+ error: "Session not found",
1598
+ messages: []
1382
1599
  });
1383
1600
  return;
1384
1601
  }
@@ -1392,7 +1609,8 @@ async function handleSessionsGet(request, log) {
1392
1609
  role: m.role,
1393
1610
  content: m.content,
1394
1611
  timestamp: m.timestamp
1395
- }))
1612
+ })),
1613
+ activity: (session.activity ?? []).map((a) => ({ ...a }))
1396
1614
  });
1397
1615
  } catch (err) {
1398
1616
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -1500,11 +1718,13 @@ const plugin = {
1500
1718
  const session = await getSession(params.sessionId);
1501
1719
  if (!session) return {
1502
1720
  ok: false,
1503
- error: "Session not found"
1721
+ error: "Session not found",
1722
+ messages: []
1504
1723
  };
1505
1724
  if (params.userId && session.userId && session.userId !== params.userId) return {
1506
1725
  ok: false,
1507
- error: "Session not found"
1726
+ error: "Session not found",
1727
+ messages: []
1508
1728
  };
1509
1729
  return {
1510
1730
  sessionId: session.sessionId,
@@ -1516,7 +1736,8 @@ const plugin = {
1516
1736
  role: m.role,
1517
1737
  content: m.content,
1518
1738
  timestamp: m.timestamp
1519
- }))
1739
+ })),
1740
+ activity: (session.activity ?? []).map((a) => ({ ...a }))
1520
1741
  };
1521
1742
  });
1522
1743
  gw.__alfeChatGatewayMethodsRegistered = true;
@@ -1598,12 +1819,24 @@ Object.defineProperty(exports, "createAlfeChannelPlugin", {
1598
1819
  return createAlfeChannelPlugin;
1599
1820
  }
1600
1821
  });
1822
+ Object.defineProperty(exports, "joinAssistantText", {
1823
+ enumerable: true,
1824
+ get: function() {
1825
+ return joinAssistantText;
1826
+ }
1827
+ });
1601
1828
  Object.defineProperty(exports, "plugin", {
1602
1829
  enumerable: true,
1603
1830
  get: function() {
1604
1831
  return plugin;
1605
1832
  }
1606
1833
  });
1834
+ Object.defineProperty(exports, "rewriteAssistantText", {
1835
+ enumerable: true,
1836
+ get: function() {
1837
+ return rewriteAssistantText;
1838
+ }
1839
+ });
1607
1840
  Object.defineProperty(exports, "stripLeakedToolSummary", {
1608
1841
  enumerable: true,
1609
1842
  get: function() {
@@ -1,2 +1,2 @@
1
- import { a as plugin, i as buildToolActivity, n as admitAgentEvent, o as stripLeakedToolSummary, r as buildA2ACompletePayload, t as RunEventGate } from "./plugin.cjs";
2
- export { RunEventGate, admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, stripLeakedToolSummary };
1
+ import { a as joinAssistantText, c as stripLeakedToolSummary, i as buildToolActivity, n as admitAgentEvent, o as plugin, r as buildA2ACompletePayload, s as rewriteAssistantText, t as RunEventGate } from "./plugin.cjs";
2
+ export { RunEventGate, admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, joinAssistantText, rewriteAssistantText, stripLeakedToolSummary };
package/dist/plugin2.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as plugin, i as buildToolActivity, n as admitAgentEvent, o as stripLeakedToolSummary, r as buildA2ACompletePayload, t as RunEventGate } from "./plugin.js";
2
- export { RunEventGate, admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, stripLeakedToolSummary };
1
+ import { a as joinAssistantText, c as stripLeakedToolSummary, i as buildToolActivity, n as admitAgentEvent, o as plugin, r as buildA2ACompletePayload, s as rewriteAssistantText, t as RunEventGate } from "./plugin.js";
2
+ export { RunEventGate, admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, joinAssistantText, rewriteAssistantText, stripLeakedToolSummary };
package/dist/plugin2.js CHANGED
@@ -344,6 +344,31 @@ async function addMessage(sessionId, role, content, senderId, senderName) {
344
344
  writeLocks.set(sessionId, next);
345
345
  await next;
346
346
  }
347
+ const MAX_ACTIVITY_PER_SESSION = 500;
348
+ const MAX_ACTIVITY_THINKING_CHARS = 16e3;
349
+ /**
350
+ * Append a turn's terminalized activity records. Called ONCE per turn (from
351
+ * the dispatch `finally`) — never per-delta: thinking deltas are
352
+ * high-frequency and each save rewrites the whole session file.
353
+ */
354
+ async function appendActivity(sessionId, entries) {
355
+ if (entries.length === 0) return;
356
+ const next = (writeLocks.get(sessionId) ?? Promise.resolve()).then(async () => {
357
+ const session = await getSession(sessionId);
358
+ if (!session) return;
359
+ const capped = entries.map((e) => e.kind === "thinking" && e.text.length > MAX_ACTIVITY_THINKING_CHARS ? {
360
+ ...e,
361
+ text: e.text.slice(0, MAX_ACTIVITY_THINKING_CHARS)
362
+ } : e);
363
+ const combined = [...session.activity ?? [], ...capped];
364
+ session.activity = combined.length > MAX_ACTIVITY_PER_SESSION ? combined.slice(combined.length - MAX_ACTIVITY_PER_SESSION) : combined;
365
+ await saveSession(session);
366
+ }).finally(() => {
367
+ if (writeLocks.get(sessionId) === next) writeLocks.delete(sessionId);
368
+ });
369
+ writeLocks.set(sessionId, next);
370
+ await next;
371
+ }
347
372
  async function listSessions(filters, limit = 50) {
348
373
  await ensureDir();
349
374
  let files;
@@ -848,6 +873,101 @@ function extractResultText(result) {
848
873
  return parts.join("\n");
849
874
  }
850
875
  //#endregion
876
+ //#region src/think-tags.ts
877
+ /**
878
+ * Inline `<think>` span handling for assistant text.
879
+ *
880
+ * Reasoning models routed through the AI proxy (DeepSeek `deepseek-*`,
881
+ * Zhipu `glm-*`, …) emit their reasoning INLINE as `<think>…</think>` inside
882
+ * the normal content stream instead of a structured thinking channel. The
883
+ * openclaw-chat plugin (0.4.0+) diverts those spans into thinking activity
884
+ * frames at the source; this relay-side copy is the DEFENSIVE layer for
885
+ * agents still running older plugin versions — leaked reasoning is dropped
886
+ * here, never synthesized into activity (that's the plugin's job).
887
+ *
888
+ * KEEP IN SYNC (byte-identical): this module exists at BOTH
889
+ * `services/chat/src/lib/think-tags.ts` (relay, defensive layer)
890
+ * `packages/openclaw-chat/src/think-tags.ts` (plugin, at-source divert)
891
+ * The published plugin cannot import service internals, so the module is
892
+ * duplicated, same as the ChatActivity shape.
893
+ *
894
+ * Known accepted limitation: a literal `<think>` in legitimate prose or a
895
+ * code fence is treated as reasoning and stripped/diverted.
896
+ */
897
+ const OPEN_TAG = "<think>";
898
+ const CLOSE_TAG = "</think>";
899
+ /**
900
+ * Split CUMULATIVE assistant text into the user-visible part and the
901
+ * reasoning part.
902
+ *
903
+ * Invariant (load-bearing): for successive cumulative snapshots that grow by
904
+ * appending, `visible` is monotonically append-only — `visible(n)` is always
905
+ * a prefix of `visible(n + 1)`. The delta relays slice by a `lastSentLength`
906
+ * offset into this string; if `visible` ever shrank or rewrote earlier
907
+ * characters, clients would receive corrupted deltas. This holds because the
908
+ * scan is deterministic left-to-right and a trailing PARTIAL tag (any strict
909
+ * prefix of the next expected tag, e.g. `"<thi"` or `"</th"`) is HELD BACK
910
+ * from both outputs until later text disambiguates it.
911
+ */
912
+ function splitThinkTags(cumulative) {
913
+ let visible = "";
914
+ let thinking = "";
915
+ let inThink = false;
916
+ let i = 0;
917
+ const n = cumulative.length;
918
+ while (i < n) {
919
+ const tag = inThink ? CLOSE_TAG : OPEN_TAG;
920
+ const idx = cumulative.indexOf(tag, i);
921
+ if (idx !== -1) {
922
+ const chunk = cumulative.slice(i, idx);
923
+ if (inThink) thinking += chunk;
924
+ else visible += chunk;
925
+ inThink = !inThink;
926
+ i = idx + tag.length;
927
+ continue;
928
+ }
929
+ let rest = cumulative.slice(i);
930
+ const held = trailingPartialTagLength(rest, tag);
931
+ if (held > 0) rest = rest.slice(0, rest.length - held);
932
+ if (inThink) thinking += rest;
933
+ else visible += rest;
934
+ break;
935
+ }
936
+ return {
937
+ visible,
938
+ thinking
939
+ };
940
+ }
941
+ /** Length of the longest strict prefix of `tag` that is a suffix of `text`. */
942
+ function trailingPartialTagLength(text, tag) {
943
+ const max = Math.min(text.length, tag.length - 1);
944
+ for (let len = max; len > 0; len--) if (text.endsWith(tag.slice(0, len))) return len;
945
+ return 0;
946
+ }
947
+ /**
948
+ * Strip `<think>…</think>` spans (and an unclosed trailing `<think>…`) from
949
+ * FINAL text, then tidy the whitespace the spans leave behind.
950
+ *
951
+ * Idempotent — `stripThinkSpans(stripThinkSpans(x)) === stripThinkSpans(x)`.
952
+ * New plugin + new relay both strip, so the second pass must be a no-op; the
953
+ * fixpoint loop also covers pathological inputs where removing one span
954
+ * makes the surrounding fragments form a new one.
955
+ *
956
+ * NOT for sliced/streaming paths — the trim + blank-line collapse breaks
957
+ * prefix alignment with `lastSentLength`; those paths use
958
+ * `splitThinkTags(...).visible` instead.
959
+ */
960
+ function stripThinkSpans(text) {
961
+ let out = text;
962
+ let prev;
963
+ do {
964
+ prev = out;
965
+ out = out.replace(/<think>[\s\S]*?<\/think>/g, "");
966
+ } while (out !== prev);
967
+ out = out.replace(/<think>[\s\S]*$/, "");
968
+ return out.replace(/\n{3,}/g, "\n\n").trim();
969
+ }
970
+ //#endregion
851
971
  //#region src/plugin.ts
852
972
  /**
853
973
  * @alfe.ai/openclaw-chat — OpenClaw chat channel plugin.
@@ -913,7 +1033,7 @@ function asString(v) {
913
1033
  * Returns null when there's nothing to forward.
914
1034
  */
915
1035
  function buildThinkingActivity(data) {
916
- const text = asString(data.delta) ?? asString(data.text) ?? asString(data.content) ?? asString(data.message?.text);
1036
+ const text = asString(data.delta) ?? asString(data.text) ?? asString(data.content) ?? asString(data.reasoning) ?? asString(data.reasoning_content) ?? asString(data.message?.text);
917
1037
  if (!text) return null;
918
1038
  return {
919
1039
  kind: "thinking",
@@ -922,6 +1042,34 @@ function buildThinkingActivity(data) {
922
1042
  };
923
1043
  }
924
1044
  /**
1045
+ * Join the text parts of an assistant-stream event's content array.
1046
+ * Assistant `evt.data` is a message object whose `content` is an array of
1047
+ * typed parts; deltas are CUMULATIVE snapshots (the relay length-compares
1048
+ * them — see adapter.ts's lastSentLength slicing). Non-array/absent content
1049
+ * yields '' (nothing to divert).
1050
+ */
1051
+ function joinAssistantText(data) {
1052
+ const content = data.content;
1053
+ if (!Array.isArray(content)) return "";
1054
+ return content.filter((c) => typeof c === "object" && c !== null && c.type === "text" && typeof c.text === "string").map((c) => c.text).join("");
1055
+ }
1056
+ /**
1057
+ * Rebuild an assistant event's message with its text parts replaced by one
1058
+ * `{type:'text', text: visible}` part. Non-text parts are preserved — the
1059
+ * relay's consumers (adapter/inject/voice-bridge) only join text parts, so
1060
+ * relative ordering against non-text items is not load-bearing.
1061
+ */
1062
+ function rewriteAssistantText(data, visible) {
1063
+ const nonText = (Array.isArray(data.content) ? data.content : []).filter((c) => !(typeof c === "object" && c !== null && c.type === "text"));
1064
+ return {
1065
+ ...data,
1066
+ content: [...nonText, {
1067
+ type: "text",
1068
+ text: visible
1069
+ }]
1070
+ };
1071
+ }
1072
+ /**
925
1073
  * Extract structured tool status from an OpenClaw `tool`-stream event.
926
1074
  * We send the emoji-free raw `name` plus a lifecycle `status` and let the
927
1075
  * web render the `🛠️ <Label>: <summary> ✓/✗` row — never OpenClaw's raw
@@ -1171,6 +1319,43 @@ async function handleAgentRequest(request, log) {
1171
1319
  runId: null,
1172
1320
  sessionKey: null
1173
1321
  };
1322
+ let lastThinkingLen = 0;
1323
+ const turnThinking = {
1324
+ text: "",
1325
+ ts: 0
1326
+ };
1327
+ const turnTools = /* @__PURE__ */ new Map();
1328
+ const recordThinking = (text, ts) => {
1329
+ if (!turnThinking.text) turnThinking.ts = ts;
1330
+ turnThinking.text += text;
1331
+ };
1332
+ const recordTool = (activity, ts) => {
1333
+ const prev = turnTools.get(activity.toolCallId);
1334
+ turnTools.set(activity.toolCallId, {
1335
+ kind: "tool",
1336
+ toolCallId: activity.toolCallId,
1337
+ name: activity.name,
1338
+ status: activity.status === "running" ? prev?.status ?? "interrupted" : activity.status,
1339
+ ...activity.summary ?? prev?.summary ? { summary: activity.summary ?? prev?.summary } : {},
1340
+ ...prev?.argsText ?? activity.argsText ? { argsText: prev?.argsText ?? activity.argsText } : {},
1341
+ ...activity.resultText ?? prev?.resultText ? { resultText: activity.resultText ?? prev?.resultText } : {},
1342
+ ...(activity.isError ?? prev?.isError) !== void 0 ? { isError: activity.isError ?? prev?.isError } : {},
1343
+ ...(activity.durationMs ?? prev?.durationMs) !== void 0 ? { durationMs: activity.durationMs ?? prev?.durationMs } : {},
1344
+ ...activity.truncated ?? prev?.truncated ? { truncated: activity.truncated ?? prev?.truncated } : {},
1345
+ ts: prev?.ts ?? ts
1346
+ });
1347
+ };
1348
+ const flushTurnActivity = async () => {
1349
+ const entries = [];
1350
+ if (turnThinking.text) entries.push({
1351
+ kind: "thinking",
1352
+ text: turnThinking.text,
1353
+ ts: turnThinking.ts
1354
+ });
1355
+ for (const tool of turnTools.values()) entries.push(tool);
1356
+ entries.sort((a, b) => a.ts - b.ts);
1357
+ await appendActivity(sessionId, entries);
1358
+ };
1174
1359
  const UPDATE_THROTTLE_MS = 250;
1175
1360
  const pendingUpdates = /* @__PURE__ */ new Map();
1176
1361
  const flushToolUpdate = (toolCallId) => {
@@ -1187,33 +1372,58 @@ async function handleAgentRequest(request, log) {
1187
1372
  const unsubscribe = runtime.events.onAgentEvent((evt) => {
1188
1373
  if (!admitAgentEvent(runGate, evt)) return;
1189
1374
  if (evt.stream === "assistant") {
1375
+ const raw = joinAssistantText(evt.data);
1376
+ const { visible, thinking } = splitThinkTags(raw);
1377
+ if (thinking.length > lastThinkingLen) {
1378
+ const thinkingDelta = thinking.slice(lastThinkingLen);
1379
+ chatClient?.sendEvent("chat-activity", {
1380
+ runId: evt.runId,
1381
+ sessionKey: legacySessionKey,
1382
+ seq: evt.seq,
1383
+ state: "activity",
1384
+ activity: {
1385
+ kind: "thinking",
1386
+ text: thinkingDelta,
1387
+ delta: true,
1388
+ ts: evt.ts,
1389
+ seq: evt.seq
1390
+ }
1391
+ });
1392
+ recordThinking(thinkingDelta, evt.ts);
1393
+ lastThinkingLen = thinking.length;
1394
+ }
1395
+ const message = visible === raw && thinking.length === 0 ? evt.data : rewriteAssistantText(evt.data, visible);
1190
1396
  chatClient?.sendEvent("chat", {
1191
1397
  runId: evt.runId,
1192
1398
  sessionKey: legacySessionKey,
1193
1399
  seq: evt.seq,
1194
1400
  state: "delta",
1195
- message: evt.data
1401
+ message
1196
1402
  });
1197
1403
  return;
1198
1404
  }
1199
1405
  if (evt.stream === "thinking") {
1200
1406
  const activity = buildThinkingActivity(evt.data);
1201
- if (activity) chatClient?.sendEvent("chat-activity", {
1202
- runId: evt.runId,
1203
- sessionKey: legacySessionKey,
1204
- seq: evt.seq,
1205
- state: "activity",
1206
- activity: {
1207
- ...activity,
1208
- ts: evt.ts,
1209
- seq: evt.seq
1210
- }
1211
- });
1407
+ if (activity) {
1408
+ chatClient?.sendEvent("chat-activity", {
1409
+ runId: evt.runId,
1410
+ sessionKey: legacySessionKey,
1411
+ seq: evt.seq,
1412
+ state: "activity",
1413
+ activity: {
1414
+ ...activity,
1415
+ ts: evt.ts,
1416
+ seq: evt.seq
1417
+ }
1418
+ });
1419
+ if (activity.kind === "thinking") recordThinking(activity.text, evt.ts);
1420
+ }
1212
1421
  return;
1213
1422
  }
1214
1423
  if (evt.stream === "tool") {
1215
1424
  const activity = buildToolActivity(evt.data);
1216
1425
  if (activity?.kind !== "tool") return;
1426
+ recordTool(activity, evt.ts);
1217
1427
  const toolCallId = activity.toolCallId;
1218
1428
  const send = () => {
1219
1429
  chatClient?.sendEvent("chat-activity", {
@@ -1309,10 +1519,10 @@ async function handleAgentRequest(request, log) {
1309
1519
  } : {}
1310
1520
  },
1311
1521
  deliver: async (payload) => {
1312
- const responseText = stripLeakedToolSummary(payload.text ?? "");
1522
+ const responseText = stripLeakedToolSummary(stripThinkSpans(payload.text ?? ""));
1313
1523
  const mediaUrls = [...payload.mediaUrl ? [payload.mediaUrl] : [], ...payload.mediaUrls ?? []];
1314
1524
  await addMessage(sessionId, "assistant", responseText);
1315
- if (isA2A) a2aResponseBuffer += responseText;
1525
+ if (isA2A) a2aResponseBuffer += (a2aResponseBuffer && responseText ? "\n\n" : "") + responseText;
1316
1526
  chatClient?.notify("agent-message", {
1317
1527
  conversationId: conversationId ?? legacySessionKey,
1318
1528
  text: responseText,
@@ -1346,6 +1556,11 @@ async function handleAgentRequest(request, log) {
1346
1556
  } finally {
1347
1557
  unsubscribe();
1348
1558
  clearAllToolUpdates();
1559
+ try {
1560
+ await flushTurnActivity();
1561
+ } catch (err) {
1562
+ log.error(`Activity persistence failed: ${err instanceof Error ? err.message : String(err)}`);
1563
+ }
1349
1564
  if (isA2A) disarmA2AEndSignal();
1350
1565
  }
1351
1566
  }
@@ -1371,14 +1586,16 @@ async function handleSessionsGet(request, log) {
1371
1586
  if (!session) {
1372
1587
  chatClient?.sendResponse(request.id, true, {
1373
1588
  ok: false,
1374
- error: "Session not found"
1589
+ error: "Session not found",
1590
+ messages: []
1375
1591
  });
1376
1592
  return;
1377
1593
  }
1378
1594
  if (params.userId && session.userId && session.userId !== params.userId) {
1379
1595
  chatClient?.sendResponse(request.id, true, {
1380
1596
  ok: false,
1381
- error: "Session not found"
1597
+ error: "Session not found",
1598
+ messages: []
1382
1599
  });
1383
1600
  return;
1384
1601
  }
@@ -1392,7 +1609,8 @@ async function handleSessionsGet(request, log) {
1392
1609
  role: m.role,
1393
1610
  content: m.content,
1394
1611
  timestamp: m.timestamp
1395
- }))
1612
+ })),
1613
+ activity: (session.activity ?? []).map((a) => ({ ...a }))
1396
1614
  });
1397
1615
  } catch (err) {
1398
1616
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -1500,11 +1718,13 @@ const plugin = {
1500
1718
  const session = await getSession(params.sessionId);
1501
1719
  if (!session) return {
1502
1720
  ok: false,
1503
- error: "Session not found"
1721
+ error: "Session not found",
1722
+ messages: []
1504
1723
  };
1505
1724
  if (params.userId && session.userId && session.userId !== params.userId) return {
1506
1725
  ok: false,
1507
- error: "Session not found"
1726
+ error: "Session not found",
1727
+ messages: []
1508
1728
  };
1509
1729
  return {
1510
1730
  sessionId: session.sessionId,
@@ -1516,7 +1736,8 @@ const plugin = {
1516
1736
  role: m.role,
1517
1737
  content: m.content,
1518
1738
  timestamp: m.timestamp
1519
- }))
1739
+ })),
1740
+ activity: (session.activity ?? []).map((a) => ({ ...a }))
1520
1741
  };
1521
1742
  });
1522
1743
  gw.__alfeChatGatewayMethodsRegistered = true;
@@ -1574,4 +1795,4 @@ const plugin = {
1574
1795
  }
1575
1796
  };
1576
1797
  //#endregion
1577
- export { stripLeakedToolSummary as a, plugin as i, buildA2ACompletePayload as n, createAlfeChannelPlugin as o, buildToolActivity as r, admitAgentEvent as t };
1798
+ export { plugin as a, createAlfeChannelPlugin as c, joinAssistantText as i, buildA2ACompletePayload as n, rewriteAssistantText as o, buildToolActivity as r, stripLeakedToolSummary as s, admitAgentEvent as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/openclaw-chat",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "OpenClaw chat plugin for Alfe — web widget and mobile app channels",
5
5
  "type": "module",
6
6
  "main": "./dist/plugin.js",
@@ -27,8 +27,8 @@
27
27
  "openclaw.plugin.json"
28
28
  ],
29
29
  "dependencies": {
30
- "@alfe.ai/agent-api-client": "^0.5.0",
31
30
  "@alfe.ai/chat": "^0.0.14",
31
+ "@alfe.ai/agent-api-client": "^0.5.0",
32
32
  "@alfe.ai/config": "^0.3.0"
33
33
  },
34
34
  "peerDependencies": {