@alfe.ai/openclaw-chat 0.2.0 → 0.2.2

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 AlfePluginConfig, i as AlfeChannelConfig, o as AlfeResolvedAccount, r as createAlfeChannelPlugin, t as plugin } from "./plugin.cjs";
1
+ import { a as AlfeChannelConfig, i as createAlfeChannelPlugin, n as plugin, o as AlfePluginConfig, s as AlfeResolvedAccount } from "./plugin.cjs";
2
2
 
3
3
  //#region src/session-store.d.ts
4
4
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as AlfePluginConfig, i as AlfeChannelConfig, o as AlfeResolvedAccount, r as createAlfeChannelPlugin, t as plugin } from "./plugin.js";
1
+ import { a as AlfeChannelConfig, i as createAlfeChannelPlugin, n as plugin, o as AlfePluginConfig, s as AlfeResolvedAccount } from "./plugin.js";
2
2
 
3
3
  //#region src/session-store.d.ts
4
4
 
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { r as createAlfeChannelPlugin, t as plugin } from "./plugin2.js";
1
+ import { i as createAlfeChannelPlugin, n as plugin } from "./plugin2.js";
2
2
  export { createAlfeChannelPlugin, plugin as default };
package/dist/plugin.cjs CHANGED
@@ -3,5 +3,6 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_plugin = require("./plugin2.cjs");
6
+ exports.buildA2ACompletePayload = require_plugin.buildA2ACompletePayload;
6
7
  exports.default = require_plugin.plugin;
7
8
  exports.stripLeakedToolSummary = require_plugin.stripLeakedToolSummary;
package/dist/plugin.d.cts CHANGED
@@ -275,6 +275,27 @@ interface PluginServiceContext {
275
275
  logger: PluginLogger;
276
276
  }
277
277
  declare function stripLeakedToolSummary(text: string): string;
278
+ /**
279
+ * Build the `a2a-complete` notify payload. ALWAYS fired for an A2A turn —
280
+ * even when the dispatch produced no relayable text — so the chat service can
281
+ * emit an explicit terminal/stop frame instead of the thread dead-ending
282
+ * silently. `terminated`/`reason` are set when there's nothing to relay
283
+ * (`empty_turn`) or the agent ended the thread via end_conversation
284
+ * (`resolved`).
285
+ */
286
+ declare function buildA2ACompletePayload<A>(params: {
287
+ conversationId: string;
288
+ fullText: string;
289
+ a2a: A;
290
+ resolved: boolean;
291
+ }): {
292
+ conversationId: string;
293
+ fullText: string;
294
+ a2a: A;
295
+ resolved: boolean;
296
+ terminated?: boolean;
297
+ reason?: string;
298
+ };
278
299
  interface PluginApi {
279
300
  logger: PluginLogger;
280
301
  registrationMode?: 'full' | 'setup-only' | 'setup-runtime' | 'cli-metadata';
@@ -307,4 +328,4 @@ declare const plugin: {
307
328
  deactivate(api: PluginApi): Promise<void>;
308
329
  };
309
330
  //#endregion
310
- export { AlfePluginConfig as a, AlfeChannelConfig as i, stripLeakedToolSummary as n, AlfeResolvedAccount as o, createAlfeChannelPlugin as r, plugin as t };
331
+ export { AlfeChannelConfig as a, createAlfeChannelPlugin as i, plugin as n, AlfePluginConfig as o, stripLeakedToolSummary as r, AlfeResolvedAccount as s, buildA2ACompletePayload as t };
package/dist/plugin.d.ts CHANGED
@@ -275,6 +275,27 @@ interface PluginServiceContext {
275
275
  logger: PluginLogger;
276
276
  }
277
277
  declare function stripLeakedToolSummary(text: string): string;
278
+ /**
279
+ * Build the `a2a-complete` notify payload. ALWAYS fired for an A2A turn —
280
+ * even when the dispatch produced no relayable text — so the chat service can
281
+ * emit an explicit terminal/stop frame instead of the thread dead-ending
282
+ * silently. `terminated`/`reason` are set when there's nothing to relay
283
+ * (`empty_turn`) or the agent ended the thread via end_conversation
284
+ * (`resolved`).
285
+ */
286
+ declare function buildA2ACompletePayload<A>(params: {
287
+ conversationId: string;
288
+ fullText: string;
289
+ a2a: A;
290
+ resolved: boolean;
291
+ }): {
292
+ conversationId: string;
293
+ fullText: string;
294
+ a2a: A;
295
+ resolved: boolean;
296
+ terminated?: boolean;
297
+ reason?: string;
298
+ };
278
299
  interface PluginApi {
279
300
  logger: PluginLogger;
280
301
  registrationMode?: 'full' | 'setup-only' | 'setup-runtime' | 'cli-metadata';
@@ -307,4 +328,4 @@ declare const plugin: {
307
328
  deactivate(api: PluginApi): Promise<void>;
308
329
  };
309
330
  //#endregion
310
- export { AlfePluginConfig as a, AlfeChannelConfig as i, stripLeakedToolSummary as n, AlfeResolvedAccount as o, createAlfeChannelPlugin as r, plugin as t };
331
+ export { AlfeChannelConfig as a, createAlfeChannelPlugin as i, plugin as n, AlfePluginConfig as o, stripLeakedToolSummary as r, AlfeResolvedAccount as s, buildA2ACompletePayload as t };
package/dist/plugin.js CHANGED
@@ -1,2 +1,2 @@
1
- import { n as stripLeakedToolSummary, t as plugin } from "./plugin2.js";
2
- export { plugin as default, stripLeakedToolSummary };
1
+ import { n as plugin, r as stripLeakedToolSummary, t as buildA2ACompletePayload } from "./plugin2.js";
2
+ export { buildA2ACompletePayload, plugin as default, stripLeakedToolSummary };
package/dist/plugin2.cjs CHANGED
@@ -382,6 +382,31 @@ async function listSessions(filters, limit = 50) {
382
382
  }
383
383
  //#endregion
384
384
  //#region src/a2a-tools.ts
385
+ let conversationEndRequested = false;
386
+ let a2aTurnArmed = false;
387
+ /**
388
+ * Clear the end signal AND arm the gate. Call immediately before an A2A
389
+ * dispatch. Arming is what permits `end_conversation` to set the flag for the
390
+ * duration of this turn.
391
+ */
392
+ function resetA2AEndSignal() {
393
+ conversationEndRequested = false;
394
+ a2aTurnArmed = true;
395
+ }
396
+ /**
397
+ * Clear the end signal AND disarm the gate. Call after an A2A dispatch (in a
398
+ * `finally`, so it also runs on error). Disarming closes the gate so a later
399
+ * NON-A2A turn invoking `end_conversation` can't set a flag the next A2A turn
400
+ * would read.
401
+ */
402
+ function disarmA2AEndSignal() {
403
+ conversationEndRequested = false;
404
+ a2aTurnArmed = false;
405
+ }
406
+ /** True if `end_conversation` was invoked since the last reset. */
407
+ function isA2AEndSignalled() {
408
+ return conversationEndRequested;
409
+ }
385
410
  function ok(result) {
386
411
  return { content: [{
387
412
  type: "text",
@@ -409,7 +434,12 @@ function defineTool(def) {
409
434
  }
410
435
  };
411
436
  }
412
- function buildA2ATools(chatClient, log) {
437
+ function buildA2ATools(getChatClient, log) {
438
+ const requireClient = () => {
439
+ const client = getChatClient();
440
+ if (!client) throw new Error("chat service not connected yet — try again in a moment");
441
+ return client;
442
+ };
413
443
  return [
414
444
  defineTool({
415
445
  name: "list_agents",
@@ -421,7 +451,7 @@ function buildA2ATools(chatClient, log) {
421
451
  },
422
452
  handler: async () => {
423
453
  log.info("list_agents tool called");
424
- return await chatClient.sendRequest("a2a.list-agents", {});
454
+ return await requireClient().sendRequest("a2a.list-agents", {});
425
455
  }
426
456
  }),
427
457
  defineTool({
@@ -456,7 +486,7 @@ function buildA2ATools(chatClient, log) {
456
486
  const maxDepth = params.max_depth ?? 10;
457
487
  if (!agentId || !message) throw new Error("agent_id and message are required");
458
488
  log.info(`message_agent tool: sending to ${agentId}`);
459
- return await chatClient.sendRequest("a2a.send", {
489
+ return await requireClient().sendRequest("a2a.send", {
460
490
  targetAgentId: agentId,
461
491
  message,
462
492
  threadId,
@@ -478,6 +508,7 @@ function buildA2ATools(chatClient, log) {
478
508
  handler: (params) => {
479
509
  const summary = params.summary;
480
510
  log.info(`end_conversation tool called${summary ? `: ${summary}` : ""}`);
511
+ if (a2aTurnArmed) conversationEndRequested = true;
481
512
  return Promise.resolve({
482
513
  status: "conversation_ended",
483
514
  summary
@@ -675,6 +706,28 @@ function stripLeakedToolSummary(text) {
675
706
  if (!text.includes("step")) return text;
676
707
  return text.split("\n").filter((line) => !LEAKED_TOOL_SUMMARY_LINE.test(line)).join("\n").replace(/\n{3,}/g, "\n\n").trim();
677
708
  }
709
+ /**
710
+ * Build the `a2a-complete` notify payload. ALWAYS fired for an A2A turn —
711
+ * even when the dispatch produced no relayable text — so the chat service can
712
+ * emit an explicit terminal/stop frame instead of the thread dead-ending
713
+ * silently. `terminated`/`reason` are set when there's nothing to relay
714
+ * (`empty_turn`) or the agent ended the thread via end_conversation
715
+ * (`resolved`).
716
+ */
717
+ function buildA2ACompletePayload(params) {
718
+ const hasText = params.fullText.trim().length > 0;
719
+ const terminal = params.resolved || !hasText;
720
+ return {
721
+ conversationId: params.conversationId,
722
+ fullText: params.fullText,
723
+ a2a: params.a2a,
724
+ resolved: params.resolved,
725
+ ...terminal ? {
726
+ terminated: true,
727
+ reason: params.resolved ? "resolved" : "empty_turn"
728
+ } : {}
729
+ };
730
+ }
678
731
  let dispatchInbound = null;
679
732
  /**
680
733
  * Resolve OpenClaw SDK functions from the running process.
@@ -778,6 +831,12 @@ async function downloadAttachments(attachments, log) {
778
831
  }
779
832
  return results;
780
833
  }
834
+ let agentTurnQueue = Promise.resolve();
835
+ function enqueueAgentTurn(task) {
836
+ const run = agentTurnQueue.then(task, task);
837
+ agentTurnQueue = run.then(() => void 0, () => void 0);
838
+ return run;
839
+ }
781
840
  async function handleAgentRequest(request, log) {
782
841
  const runtime = pluginRuntime;
783
842
  if (!runtime) {
@@ -859,7 +918,7 @@ async function handleAgentRequest(request, log) {
859
918
  const userLabel = displayName ?? userId ?? senderId;
860
919
  const conversationLabel = conversationType === "group" ? shortConvId ? `[${channelLabel}] Group (${shortConvId})` : `[${channelLabel}] Group` : shortConvId ? `[${channelLabel}] ${userLabel} (${shortConvId})` : `[${channelLabel}] ${userLabel}`;
861
920
  let a2aResponseBuffer = "";
862
- let a2aResolved = false;
921
+ if (isA2A) resetA2AEndSignal();
863
922
  resolvedOpenClawKey = (await dispatchInbound({
864
923
  cfg,
865
924
  runtime: { channel: runtime.channel },
@@ -899,7 +958,8 @@ async function handleAgentRequest(request, log) {
899
958
  `This is an agent-to-agent conversation with ${a2a.sourceAgentName}.`,
900
959
  "Rules:",
901
960
  "- Only respond if you have new information, a question, or an action to coordinate.",
902
- "- If the conversation is complete, call the end_conversation() tool OR end your message with [RESOLVED].",
961
+ "- When the discussion is complete, call the end_conversation() tool AND end your final message with the literal token [RESOLVED] (uppercase, in square brackets, as the last thing in the message).",
962
+ "- Do NOT write the word \"resolved\" in prose unless you mean to end — only the exact token [RESOLVED] ends the thread.",
903
963
  "- Do NOT respond just to acknowledge — that creates infinite loops."
904
964
  ].join("\n")
905
965
  } : {}
@@ -908,10 +968,7 @@ async function handleAgentRequest(request, log) {
908
968
  const responseText = stripLeakedToolSummary(payload.text ?? "");
909
969
  const mediaUrls = [...payload.mediaUrl ? [payload.mediaUrl] : [], ...payload.mediaUrls ?? []];
910
970
  await addMessage(sessionId, "assistant", responseText);
911
- if (isA2A) {
912
- a2aResponseBuffer += responseText;
913
- if (responseText.includes("\"status\":\"conversation_ended\"") || responseText.includes("\"status\": \"conversation_ended\"")) a2aResolved = true;
914
- }
971
+ if (isA2A) a2aResponseBuffer += responseText;
915
972
  chatClient?.notify("agent-message", {
916
973
  conversationId: conversationId ?? legacySessionKey,
917
974
  text: responseText,
@@ -927,12 +984,15 @@ async function handleAgentRequest(request, log) {
927
984
  log.error(`Dispatch error (${info.kind}): ${err instanceof Error ? err.message : String(err)}`);
928
985
  }
929
986
  })).route.sessionKey;
930
- if (isA2A && a2aResponseBuffer) chatClient?.notify("a2a-complete", {
931
- conversationId: conversationId ?? legacySessionKey,
932
- fullText: a2aResponseBuffer,
933
- a2a,
934
- resolved: a2aResolved
935
- });
987
+ if (isA2A) {
988
+ const a2aResolved = isA2AEndSignalled();
989
+ chatClient?.notify("a2a-complete", buildA2ACompletePayload({
990
+ conversationId: conversationId ?? legacySessionKey,
991
+ fullText: a2aResponseBuffer,
992
+ a2a,
993
+ resolved: a2aResolved
994
+ }));
995
+ }
936
996
  chatClient?.sendResponse(request.id, true, { sessionKey: resolvedOpenClawKey });
937
997
  log.info(`Agent dispatch complete: sessionKey=${resolvedOpenClawKey}`);
938
998
  } catch (err) {
@@ -941,6 +1001,7 @@ async function handleAgentRequest(request, log) {
941
1001
  chatClient?.sendResponse(request.id, false, { message: errMsg });
942
1002
  } finally {
943
1003
  unsubscribe();
1004
+ if (isA2A) disarmA2AEndSignal();
944
1005
  }
945
1006
  }
946
1007
  async function handleSessionsList(request, log) {
@@ -1010,6 +1071,11 @@ const plugin = {
1010
1071
  });
1011
1072
  api.registerChannel(alfeChannel);
1012
1073
  log.info(`Registered channel: ${alfeChannel.id}`);
1074
+ if (typeof api.registerTool === "function") {
1075
+ const a2aTools = buildA2ATools(() => chatClient, log);
1076
+ for (const tool of a2aTools) api.registerTool(tool);
1077
+ log.info(`Registered ${String(a2aTools.length)} agent-to-agent tools`);
1078
+ }
1013
1079
  const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
1014
1080
  const startChatService = () => {
1015
1081
  if (globalThis.__alfeChatPluginActivated === true) {
@@ -1033,7 +1099,7 @@ const plugin = {
1033
1099
  apiKey,
1034
1100
  onRequest: (request) => {
1035
1101
  const handle = async () => {
1036
- if (request.method === "agent") await handleAgentRequest(request, log);
1102
+ if (request.method === "agent") await enqueueAgentTurn(() => handleAgentRequest(request, log));
1037
1103
  else if (request.method === "sessions.list") await handleSessionsList(request, log);
1038
1104
  else if (request.method === "sessions.get") await handleSessionsGet(request, log);
1039
1105
  else chatClient?.sendResponse(request.id, false, { message: `Unknown method: ${request.method}` });
@@ -1056,13 +1122,6 @@ const plugin = {
1056
1122
  log.error(`Failed to initialize chat service: ${err instanceof Error ? err.message : String(err)}`);
1057
1123
  }
1058
1124
  });
1059
- if (typeof api.registerTool === "function") connectingPromise.then(() => {
1060
- if (chatClient && typeof api.registerTool === "function") {
1061
- const a2aTools = buildA2ATools(chatClient, log);
1062
- for (const tool of a2aTools) api.registerTool(tool);
1063
- log.info(`Registered ${String(a2aTools.length)} agent-to-agent tools`);
1064
- }
1065
- }).catch(() => {});
1066
1125
  };
1067
1126
  const stopChatService = async () => {
1068
1127
  globalThis.__alfeChatPluginActivated = false;
@@ -1168,6 +1227,12 @@ const plugin = {
1168
1227
  }
1169
1228
  };
1170
1229
  //#endregion
1230
+ Object.defineProperty(exports, "buildA2ACompletePayload", {
1231
+ enumerable: true,
1232
+ get: function() {
1233
+ return buildA2ACompletePayload;
1234
+ }
1235
+ });
1171
1236
  Object.defineProperty(exports, "createAlfeChannelPlugin", {
1172
1237
  enumerable: true,
1173
1238
  get: function() {
@@ -1,2 +1,2 @@
1
- import { n as stripLeakedToolSummary, t as plugin } from "./plugin.cjs";
2
- export { plugin as default, stripLeakedToolSummary };
1
+ import { n as plugin, r as stripLeakedToolSummary, t as buildA2ACompletePayload } from "./plugin.cjs";
2
+ export { buildA2ACompletePayload, plugin as default, stripLeakedToolSummary };
package/dist/plugin2.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { n as stripLeakedToolSummary, t as plugin } from "./plugin.js";
2
- export { plugin as default, stripLeakedToolSummary };
1
+ import { n as plugin, r as stripLeakedToolSummary, t as buildA2ACompletePayload } from "./plugin.js";
2
+ export { buildA2ACompletePayload, plugin as default, stripLeakedToolSummary };
package/dist/plugin2.js CHANGED
@@ -382,6 +382,31 @@ async function listSessions(filters, limit = 50) {
382
382
  }
383
383
  //#endregion
384
384
  //#region src/a2a-tools.ts
385
+ let conversationEndRequested = false;
386
+ let a2aTurnArmed = false;
387
+ /**
388
+ * Clear the end signal AND arm the gate. Call immediately before an A2A
389
+ * dispatch. Arming is what permits `end_conversation` to set the flag for the
390
+ * duration of this turn.
391
+ */
392
+ function resetA2AEndSignal() {
393
+ conversationEndRequested = false;
394
+ a2aTurnArmed = true;
395
+ }
396
+ /**
397
+ * Clear the end signal AND disarm the gate. Call after an A2A dispatch (in a
398
+ * `finally`, so it also runs on error). Disarming closes the gate so a later
399
+ * NON-A2A turn invoking `end_conversation` can't set a flag the next A2A turn
400
+ * would read.
401
+ */
402
+ function disarmA2AEndSignal() {
403
+ conversationEndRequested = false;
404
+ a2aTurnArmed = false;
405
+ }
406
+ /** True if `end_conversation` was invoked since the last reset. */
407
+ function isA2AEndSignalled() {
408
+ return conversationEndRequested;
409
+ }
385
410
  function ok(result) {
386
411
  return { content: [{
387
412
  type: "text",
@@ -409,7 +434,12 @@ function defineTool(def) {
409
434
  }
410
435
  };
411
436
  }
412
- function buildA2ATools(chatClient, log) {
437
+ function buildA2ATools(getChatClient, log) {
438
+ const requireClient = () => {
439
+ const client = getChatClient();
440
+ if (!client) throw new Error("chat service not connected yet — try again in a moment");
441
+ return client;
442
+ };
413
443
  return [
414
444
  defineTool({
415
445
  name: "list_agents",
@@ -421,7 +451,7 @@ function buildA2ATools(chatClient, log) {
421
451
  },
422
452
  handler: async () => {
423
453
  log.info("list_agents tool called");
424
- return await chatClient.sendRequest("a2a.list-agents", {});
454
+ return await requireClient().sendRequest("a2a.list-agents", {});
425
455
  }
426
456
  }),
427
457
  defineTool({
@@ -456,7 +486,7 @@ function buildA2ATools(chatClient, log) {
456
486
  const maxDepth = params.max_depth ?? 10;
457
487
  if (!agentId || !message) throw new Error("agent_id and message are required");
458
488
  log.info(`message_agent tool: sending to ${agentId}`);
459
- return await chatClient.sendRequest("a2a.send", {
489
+ return await requireClient().sendRequest("a2a.send", {
460
490
  targetAgentId: agentId,
461
491
  message,
462
492
  threadId,
@@ -478,6 +508,7 @@ function buildA2ATools(chatClient, log) {
478
508
  handler: (params) => {
479
509
  const summary = params.summary;
480
510
  log.info(`end_conversation tool called${summary ? `: ${summary}` : ""}`);
511
+ if (a2aTurnArmed) conversationEndRequested = true;
481
512
  return Promise.resolve({
482
513
  status: "conversation_ended",
483
514
  summary
@@ -675,6 +706,28 @@ function stripLeakedToolSummary(text) {
675
706
  if (!text.includes("step")) return text;
676
707
  return text.split("\n").filter((line) => !LEAKED_TOOL_SUMMARY_LINE.test(line)).join("\n").replace(/\n{3,}/g, "\n\n").trim();
677
708
  }
709
+ /**
710
+ * Build the `a2a-complete` notify payload. ALWAYS fired for an A2A turn —
711
+ * even when the dispatch produced no relayable text — so the chat service can
712
+ * emit an explicit terminal/stop frame instead of the thread dead-ending
713
+ * silently. `terminated`/`reason` are set when there's nothing to relay
714
+ * (`empty_turn`) or the agent ended the thread via end_conversation
715
+ * (`resolved`).
716
+ */
717
+ function buildA2ACompletePayload(params) {
718
+ const hasText = params.fullText.trim().length > 0;
719
+ const terminal = params.resolved || !hasText;
720
+ return {
721
+ conversationId: params.conversationId,
722
+ fullText: params.fullText,
723
+ a2a: params.a2a,
724
+ resolved: params.resolved,
725
+ ...terminal ? {
726
+ terminated: true,
727
+ reason: params.resolved ? "resolved" : "empty_turn"
728
+ } : {}
729
+ };
730
+ }
678
731
  let dispatchInbound = null;
679
732
  /**
680
733
  * Resolve OpenClaw SDK functions from the running process.
@@ -778,6 +831,12 @@ async function downloadAttachments(attachments, log) {
778
831
  }
779
832
  return results;
780
833
  }
834
+ let agentTurnQueue = Promise.resolve();
835
+ function enqueueAgentTurn(task) {
836
+ const run = agentTurnQueue.then(task, task);
837
+ agentTurnQueue = run.then(() => void 0, () => void 0);
838
+ return run;
839
+ }
781
840
  async function handleAgentRequest(request, log) {
782
841
  const runtime = pluginRuntime;
783
842
  if (!runtime) {
@@ -859,7 +918,7 @@ async function handleAgentRequest(request, log) {
859
918
  const userLabel = displayName ?? userId ?? senderId;
860
919
  const conversationLabel = conversationType === "group" ? shortConvId ? `[${channelLabel}] Group (${shortConvId})` : `[${channelLabel}] Group` : shortConvId ? `[${channelLabel}] ${userLabel} (${shortConvId})` : `[${channelLabel}] ${userLabel}`;
861
920
  let a2aResponseBuffer = "";
862
- let a2aResolved = false;
921
+ if (isA2A) resetA2AEndSignal();
863
922
  resolvedOpenClawKey = (await dispatchInbound({
864
923
  cfg,
865
924
  runtime: { channel: runtime.channel },
@@ -899,7 +958,8 @@ async function handleAgentRequest(request, log) {
899
958
  `This is an agent-to-agent conversation with ${a2a.sourceAgentName}.`,
900
959
  "Rules:",
901
960
  "- Only respond if you have new information, a question, or an action to coordinate.",
902
- "- If the conversation is complete, call the end_conversation() tool OR end your message with [RESOLVED].",
961
+ "- When the discussion is complete, call the end_conversation() tool AND end your final message with the literal token [RESOLVED] (uppercase, in square brackets, as the last thing in the message).",
962
+ "- Do NOT write the word \"resolved\" in prose unless you mean to end — only the exact token [RESOLVED] ends the thread.",
903
963
  "- Do NOT respond just to acknowledge — that creates infinite loops."
904
964
  ].join("\n")
905
965
  } : {}
@@ -908,10 +968,7 @@ async function handleAgentRequest(request, log) {
908
968
  const responseText = stripLeakedToolSummary(payload.text ?? "");
909
969
  const mediaUrls = [...payload.mediaUrl ? [payload.mediaUrl] : [], ...payload.mediaUrls ?? []];
910
970
  await addMessage(sessionId, "assistant", responseText);
911
- if (isA2A) {
912
- a2aResponseBuffer += responseText;
913
- if (responseText.includes("\"status\":\"conversation_ended\"") || responseText.includes("\"status\": \"conversation_ended\"")) a2aResolved = true;
914
- }
971
+ if (isA2A) a2aResponseBuffer += responseText;
915
972
  chatClient?.notify("agent-message", {
916
973
  conversationId: conversationId ?? legacySessionKey,
917
974
  text: responseText,
@@ -927,12 +984,15 @@ async function handleAgentRequest(request, log) {
927
984
  log.error(`Dispatch error (${info.kind}): ${err instanceof Error ? err.message : String(err)}`);
928
985
  }
929
986
  })).route.sessionKey;
930
- if (isA2A && a2aResponseBuffer) chatClient?.notify("a2a-complete", {
931
- conversationId: conversationId ?? legacySessionKey,
932
- fullText: a2aResponseBuffer,
933
- a2a,
934
- resolved: a2aResolved
935
- });
987
+ if (isA2A) {
988
+ const a2aResolved = isA2AEndSignalled();
989
+ chatClient?.notify("a2a-complete", buildA2ACompletePayload({
990
+ conversationId: conversationId ?? legacySessionKey,
991
+ fullText: a2aResponseBuffer,
992
+ a2a,
993
+ resolved: a2aResolved
994
+ }));
995
+ }
936
996
  chatClient?.sendResponse(request.id, true, { sessionKey: resolvedOpenClawKey });
937
997
  log.info(`Agent dispatch complete: sessionKey=${resolvedOpenClawKey}`);
938
998
  } catch (err) {
@@ -941,6 +1001,7 @@ async function handleAgentRequest(request, log) {
941
1001
  chatClient?.sendResponse(request.id, false, { message: errMsg });
942
1002
  } finally {
943
1003
  unsubscribe();
1004
+ if (isA2A) disarmA2AEndSignal();
944
1005
  }
945
1006
  }
946
1007
  async function handleSessionsList(request, log) {
@@ -1010,6 +1071,11 @@ const plugin = {
1010
1071
  });
1011
1072
  api.registerChannel(alfeChannel);
1012
1073
  log.info(`Registered channel: ${alfeChannel.id}`);
1074
+ if (typeof api.registerTool === "function") {
1075
+ const a2aTools = buildA2ATools(() => chatClient, log);
1076
+ for (const tool of a2aTools) api.registerTool(tool);
1077
+ log.info(`Registered ${String(a2aTools.length)} agent-to-agent tools`);
1078
+ }
1013
1079
  const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
1014
1080
  const startChatService = () => {
1015
1081
  if (globalThis.__alfeChatPluginActivated === true) {
@@ -1033,7 +1099,7 @@ const plugin = {
1033
1099
  apiKey,
1034
1100
  onRequest: (request) => {
1035
1101
  const handle = async () => {
1036
- if (request.method === "agent") await handleAgentRequest(request, log);
1102
+ if (request.method === "agent") await enqueueAgentTurn(() => handleAgentRequest(request, log));
1037
1103
  else if (request.method === "sessions.list") await handleSessionsList(request, log);
1038
1104
  else if (request.method === "sessions.get") await handleSessionsGet(request, log);
1039
1105
  else chatClient?.sendResponse(request.id, false, { message: `Unknown method: ${request.method}` });
@@ -1056,13 +1122,6 @@ const plugin = {
1056
1122
  log.error(`Failed to initialize chat service: ${err instanceof Error ? err.message : String(err)}`);
1057
1123
  }
1058
1124
  });
1059
- if (typeof api.registerTool === "function") connectingPromise.then(() => {
1060
- if (chatClient && typeof api.registerTool === "function") {
1061
- const a2aTools = buildA2ATools(chatClient, log);
1062
- for (const tool of a2aTools) api.registerTool(tool);
1063
- log.info(`Registered ${String(a2aTools.length)} agent-to-agent tools`);
1064
- }
1065
- }).catch(() => {});
1066
1125
  };
1067
1126
  const stopChatService = async () => {
1068
1127
  globalThis.__alfeChatPluginActivated = false;
@@ -1168,4 +1227,4 @@ const plugin = {
1168
1227
  }
1169
1228
  };
1170
1229
  //#endregion
1171
- export { stripLeakedToolSummary as n, createAlfeChannelPlugin as r, plugin as t };
1230
+ export { createAlfeChannelPlugin as i, plugin as n, stripLeakedToolSummary as r, buildA2ACompletePayload as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/openclaw-chat",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "OpenClaw chat plugin for Alfe — web widget and mobile app channels",
5
5
  "type": "module",
6
6
  "main": "./dist/plugin.js",
@@ -28,8 +28,8 @@
28
28
  ],
29
29
  "dependencies": {
30
30
  "@alfe.ai/agent-api-client": "^0.2.2",
31
- "@alfe.ai/chat": "^0.0.9",
32
- "@alfe.ai/config": "^0.0.9"
31
+ "@alfe.ai/config": "^0.0.9",
32
+ "@alfe.ai/chat": "^0.0.10"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "openclaw": ">=2026.3.0"