@alfe.ai/openclaw-chat 0.9.13 → 0.9.15

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,2 +1,2 @@
1
- import { A as SessionData, D as AlfePluginConfig, E as AlfeChannelConfig, O as AlfeResolvedAccount, T as createAlfeChannelPlugin, b as plugin, j as SessionSummary, k as ChatMessage } from "./plugin.cjs";
1
+ import { A as AlfeResolvedAccount, D as createAlfeChannelPlugin, M as SessionData, N as SessionSummary, O as AlfeChannelConfig, b as plugin, j as ChatMessage, k as AlfePluginConfig } from "./plugin.cjs";
2
2
  export { type AlfeChannelConfig, type AlfePluginConfig, type AlfeResolvedAccount, type ChatMessage, type SessionData, type SessionSummary, createAlfeChannelPlugin, plugin as default };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { A as SessionData, D as AlfePluginConfig, E as AlfeChannelConfig, O as AlfeResolvedAccount, T as createAlfeChannelPlugin, b as plugin, j as SessionSummary, k as ChatMessage } from "./plugin.js";
1
+ import { A as AlfeResolvedAccount, D as createAlfeChannelPlugin, M as SessionData, N as SessionSummary, O as AlfeChannelConfig, b as plugin, j as ChatMessage, k as AlfePluginConfig } from "./plugin.js";
2
2
  export { type AlfeChannelConfig, type AlfePluginConfig, type AlfeResolvedAccount, type ChatMessage, type SessionData, type SessionSummary, createAlfeChannelPlugin, plugin as default };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { S as createAlfeChannelPlugin, _ as plugin } from "./plugin2.js";
1
+ import { _ as plugin, w as createAlfeChannelPlugin } from "./plugin2.js";
2
2
  export { createAlfeChannelPlugin, plugin as default };
package/dist/plugin.cjs CHANGED
@@ -22,5 +22,7 @@ exports.default = require_plugin.plugin;
22
22
  exports.joinAssistantText = require_plugin.joinAssistantText;
23
23
  exports.readAttachmentBodyBounded = require_plugin.readAttachmentBodyBounded;
24
24
  exports.resolveAbortTargetKeys = require_plugin.resolveAbortTargetKeys;
25
+ exports.resolveRequestedRunId = require_plugin.resolveRequestedRunId;
25
26
  exports.rewriteAssistantText = require_plugin.rewriteAssistantText;
26
27
  exports.stripLeakedToolSummary = require_plugin.stripLeakedToolSummary;
28
+ exports.withReplyRunId = require_plugin.withReplyRunId;
package/dist/plugin.d.cts CHANGED
@@ -214,6 +214,7 @@ interface AlfeOutboundSessionSummary {
214
214
  interface AlfeOutboundSession {
215
215
  sessionId: string;
216
216
  userId?: string;
217
+ tenantId?: string;
217
218
  }
218
219
  interface AlfeChannelOutboundDeps {
219
220
  getChatClient: () => AlfeOutboundChatClient | null;
@@ -480,6 +481,13 @@ interface RunEventGate {
480
481
  * pinned against real 2026.6.10 event shapes.
481
482
  */
482
483
  declare function admitAgentEvent(gate: RunEventGate, evt: Pick<AgentEventPayload, 'runId' | 'stream' | 'sessionKey'>): boolean;
484
+ /**
485
+ * Seed OpenClaw's runtime/event id with the chat service request that owns the
486
+ * turn. Older callers omit the idempotency key and retain a runtime-owned id.
487
+ */
488
+ declare function resolveRequestedRunId(idempotencyKey: unknown): string | null;
489
+ /** Preserve OpenClaw-provided reply options while seeding this exact run id. */
490
+ declare function withReplyRunId(params: Record<string, unknown>, runId: string): Record<string, unknown>;
483
491
  interface PluginServiceContext {
484
492
  config: Record<string, unknown>;
485
493
  workspaceDir?: string;
@@ -667,6 +675,7 @@ declare function buildAttachmentLocalFilename(att: Pick<AttachmentRef, 'id' | 'f
667
675
  declare function enqueueAgentTurn(task: () => Promise<void>, opts?: {
668
676
  coalesceKey?: string;
669
677
  onSuperseded?: () => void;
678
+ onStarted?: () => void;
670
679
  }): Promise<void>;
671
680
  /**
672
681
  * Test-only: reset the agent-turn backlog between unit tests. Not used at
@@ -681,6 +690,7 @@ declare const __agentTurnQueueForTest: {
681
690
  interface ActiveRunRef {
682
691
  requestSessionKey: string;
683
692
  routeSessionKey: string | null;
693
+ runId?: string;
684
694
  }
685
695
  /** Test-only: set/clear the active-run ref for handleChatAbort unit tests. */
686
696
  declare function __setActiveRunForTest(ref: ActiveRunRef | null): void;
@@ -692,7 +702,7 @@ declare function __setActiveRunForTest(ref: ActiveRunRef | null): void;
692
702
  * sessionKey that does NOT match the running turn yields no candidates — we
693
703
  * never abort another session's run on a shared daemon.
694
704
  */
695
- declare function resolveAbortTargetKeys(active: ActiveRunRef | null, abortSessionKey: string | undefined): string[];
705
+ declare function resolveAbortTargetKeys(active: ActiveRunRef | null, abortSessionKey: string | undefined, abortRunId?: string): string[];
696
706
  /**
697
707
  * Speech-style reply guidance injected into the agent's SYSTEM PROMPT for a
698
708
  * live voice-call turn (`origin:'voice'`). The user is speaking on a call and
@@ -755,4 +765,4 @@ declare const plugin: {
755
765
  deactivate(api: PluginApi): Promise<void>;
756
766
  };
757
767
  //#endregion
758
- export { SessionData as A, rewriteAssistantText as C, AlfePluginConfig as D, AlfeChannelConfig as E, AlfeResolvedAccount as O, resolveAbortTargetKeys as S, createAlfeChannelPlugin as T, computeOpenClawSdkAnchors as _, MAX_INBOUND_ATTACHMENT_SIZE as a, plugin as b, __agentTurnQueueForTest as c, admitAgentEvent as d, buildA2ACompletePayload as f, buildTurnEnvelopeContext as g, buildToolActivity as h, CROSS_AGENT_TOOLS_SYSTEM_PROMPT as i, SessionSummary as j, ChatMessage as k, __resetAgentTurnQueueForTest as l, buildOriginEnvelopeContext as m, ActiveRunRef as n, RunEventGate as o, buildAttachmentLocalFilename as p, AnchorEnv as r, VOICE_REPLY_SYSTEM_PROMPT as s, A2A_REPLY_SYSTEM_PROMPT as t, __setActiveRunForTest as u, decideChatServiceStart as v, stripLeakedToolSummary as w, readAttachmentBodyBounded as x, joinAssistantText as y };
768
+ export { AlfeResolvedAccount as A, resolveRequestedRunId as C, createAlfeChannelPlugin as D, withReplyRunId as E, SessionData as M, SessionSummary as N, AlfeChannelConfig as O, resolveAbortTargetKeys as S, stripLeakedToolSummary as T, computeOpenClawSdkAnchors as _, MAX_INBOUND_ATTACHMENT_SIZE as a, plugin as b, __agentTurnQueueForTest as c, admitAgentEvent as d, buildA2ACompletePayload as f, buildTurnEnvelopeContext as g, buildToolActivity as h, CROSS_AGENT_TOOLS_SYSTEM_PROMPT as i, ChatMessage as j, AlfePluginConfig as k, __resetAgentTurnQueueForTest as l, buildOriginEnvelopeContext as m, ActiveRunRef as n, RunEventGate as o, buildAttachmentLocalFilename as p, AnchorEnv as r, VOICE_REPLY_SYSTEM_PROMPT as s, A2A_REPLY_SYSTEM_PROMPT as t, __setActiveRunForTest as u, decideChatServiceStart as v, rewriteAssistantText as w, readAttachmentBodyBounded as x, joinAssistantText as y };
package/dist/plugin.d.ts CHANGED
@@ -214,6 +214,7 @@ interface AlfeOutboundSessionSummary {
214
214
  interface AlfeOutboundSession {
215
215
  sessionId: string;
216
216
  userId?: string;
217
+ tenantId?: string;
217
218
  }
218
219
  interface AlfeChannelOutboundDeps {
219
220
  getChatClient: () => AlfeOutboundChatClient | null;
@@ -480,6 +481,13 @@ interface RunEventGate {
480
481
  * pinned against real 2026.6.10 event shapes.
481
482
  */
482
483
  declare function admitAgentEvent(gate: RunEventGate, evt: Pick<AgentEventPayload, 'runId' | 'stream' | 'sessionKey'>): boolean;
484
+ /**
485
+ * Seed OpenClaw's runtime/event id with the chat service request that owns the
486
+ * turn. Older callers omit the idempotency key and retain a runtime-owned id.
487
+ */
488
+ declare function resolveRequestedRunId(idempotencyKey: unknown): string | null;
489
+ /** Preserve OpenClaw-provided reply options while seeding this exact run id. */
490
+ declare function withReplyRunId(params: Record<string, unknown>, runId: string): Record<string, unknown>;
483
491
  interface PluginServiceContext {
484
492
  config: Record<string, unknown>;
485
493
  workspaceDir?: string;
@@ -667,6 +675,7 @@ declare function buildAttachmentLocalFilename(att: Pick<AttachmentRef, 'id' | 'f
667
675
  declare function enqueueAgentTurn(task: () => Promise<void>, opts?: {
668
676
  coalesceKey?: string;
669
677
  onSuperseded?: () => void;
678
+ onStarted?: () => void;
670
679
  }): Promise<void>;
671
680
  /**
672
681
  * Test-only: reset the agent-turn backlog between unit tests. Not used at
@@ -681,6 +690,7 @@ declare const __agentTurnQueueForTest: {
681
690
  interface ActiveRunRef {
682
691
  requestSessionKey: string;
683
692
  routeSessionKey: string | null;
693
+ runId?: string;
684
694
  }
685
695
  /** Test-only: set/clear the active-run ref for handleChatAbort unit tests. */
686
696
  declare function __setActiveRunForTest(ref: ActiveRunRef | null): void;
@@ -692,7 +702,7 @@ declare function __setActiveRunForTest(ref: ActiveRunRef | null): void;
692
702
  * sessionKey that does NOT match the running turn yields no candidates — we
693
703
  * never abort another session's run on a shared daemon.
694
704
  */
695
- declare function resolveAbortTargetKeys(active: ActiveRunRef | null, abortSessionKey: string | undefined): string[];
705
+ declare function resolveAbortTargetKeys(active: ActiveRunRef | null, abortSessionKey: string | undefined, abortRunId?: string): string[];
696
706
  /**
697
707
  * Speech-style reply guidance injected into the agent's SYSTEM PROMPT for a
698
708
  * live voice-call turn (`origin:'voice'`). The user is speaking on a call and
@@ -755,4 +765,4 @@ declare const plugin: {
755
765
  deactivate(api: PluginApi): Promise<void>;
756
766
  };
757
767
  //#endregion
758
- export { SessionData as A, rewriteAssistantText as C, AlfePluginConfig as D, AlfeChannelConfig as E, AlfeResolvedAccount as O, resolveAbortTargetKeys as S, createAlfeChannelPlugin as T, computeOpenClawSdkAnchors as _, MAX_INBOUND_ATTACHMENT_SIZE as a, plugin as b, __agentTurnQueueForTest as c, admitAgentEvent as d, buildA2ACompletePayload as f, buildTurnEnvelopeContext as g, buildToolActivity as h, CROSS_AGENT_TOOLS_SYSTEM_PROMPT as i, SessionSummary as j, ChatMessage as k, __resetAgentTurnQueueForTest as l, buildOriginEnvelopeContext as m, ActiveRunRef as n, RunEventGate as o, buildAttachmentLocalFilename as p, AnchorEnv as r, VOICE_REPLY_SYSTEM_PROMPT as s, A2A_REPLY_SYSTEM_PROMPT as t, __setActiveRunForTest as u, decideChatServiceStart as v, stripLeakedToolSummary as w, readAttachmentBodyBounded as x, joinAssistantText as y };
768
+ export { AlfeResolvedAccount as A, resolveRequestedRunId as C, createAlfeChannelPlugin as D, withReplyRunId as E, SessionData as M, SessionSummary as N, AlfeChannelConfig as O, resolveAbortTargetKeys as S, stripLeakedToolSummary as T, computeOpenClawSdkAnchors as _, MAX_INBOUND_ATTACHMENT_SIZE as a, plugin as b, __agentTurnQueueForTest as c, admitAgentEvent as d, buildA2ACompletePayload as f, buildTurnEnvelopeContext as g, buildToolActivity as h, CROSS_AGENT_TOOLS_SYSTEM_PROMPT as i, ChatMessage as j, AlfePluginConfig as k, __resetAgentTurnQueueForTest as l, buildOriginEnvelopeContext as m, ActiveRunRef as n, RunEventGate as o, buildAttachmentLocalFilename as p, AnchorEnv as r, VOICE_REPLY_SYSTEM_PROMPT as s, A2A_REPLY_SYSTEM_PROMPT as t, __setActiveRunForTest as u, decideChatServiceStart as v, rewriteAssistantText as w, readAttachmentBodyBounded as x, joinAssistantText as y };
package/dist/plugin.js CHANGED
@@ -1,2 +1,2 @@
1
- import { _ as plugin, a as __agentTurnQueueForTest, b as rewriteAssistantText, c as admitAgentEvent, d as buildOriginEnvelopeContext, f as buildToolActivity, g as joinAssistantText, h as decideChatServiceStart, i as VOICE_REPLY_SYSTEM_PROMPT, l as buildA2ACompletePayload, m as computeOpenClawSdkAnchors, n as CROSS_AGENT_TOOLS_SYSTEM_PROMPT, o as __resetAgentTurnQueueForTest, p as buildTurnEnvelopeContext, r as MAX_INBOUND_ATTACHMENT_SIZE, s as __setActiveRunForTest, t as A2A_REPLY_SYSTEM_PROMPT, u as buildAttachmentLocalFilename, v as readAttachmentBodyBounded, x as stripLeakedToolSummary, y as resolveAbortTargetKeys } from "./plugin2.js";
2
- export { A2A_REPLY_SYSTEM_PROMPT, CROSS_AGENT_TOOLS_SYSTEM_PROMPT, MAX_INBOUND_ATTACHMENT_SIZE, VOICE_REPLY_SYSTEM_PROMPT, __agentTurnQueueForTest, __resetAgentTurnQueueForTest, __setActiveRunForTest, admitAgentEvent, buildA2ACompletePayload, buildAttachmentLocalFilename, buildOriginEnvelopeContext, buildToolActivity, buildTurnEnvelopeContext, computeOpenClawSdkAnchors, decideChatServiceStart, plugin as default, joinAssistantText, readAttachmentBodyBounded, resolveAbortTargetKeys, rewriteAssistantText, stripLeakedToolSummary };
1
+ import { C as withReplyRunId, S as stripLeakedToolSummary, _ as plugin, a as __agentTurnQueueForTest, b as resolveRequestedRunId, c as admitAgentEvent, d as buildOriginEnvelopeContext, f as buildToolActivity, g as joinAssistantText, h as decideChatServiceStart, i as VOICE_REPLY_SYSTEM_PROMPT, l as buildA2ACompletePayload, m as computeOpenClawSdkAnchors, n as CROSS_AGENT_TOOLS_SYSTEM_PROMPT, o as __resetAgentTurnQueueForTest, p as buildTurnEnvelopeContext, r as MAX_INBOUND_ATTACHMENT_SIZE, s as __setActiveRunForTest, t as A2A_REPLY_SYSTEM_PROMPT, u as buildAttachmentLocalFilename, v as readAttachmentBodyBounded, x as rewriteAssistantText, y as resolveAbortTargetKeys } from "./plugin2.js";
2
+ export { A2A_REPLY_SYSTEM_PROMPT, CROSS_AGENT_TOOLS_SYSTEM_PROMPT, MAX_INBOUND_ATTACHMENT_SIZE, VOICE_REPLY_SYSTEM_PROMPT, __agentTurnQueueForTest, __resetAgentTurnQueueForTest, __setActiveRunForTest, admitAgentEvent, buildA2ACompletePayload, buildAttachmentLocalFilename, buildOriginEnvelopeContext, buildToolActivity, buildTurnEnvelopeContext, computeOpenClawSdkAnchors, decideChatServiceStart, plugin as default, joinAssistantText, readAttachmentBodyBounded, resolveAbortTargetKeys, resolveRequestedRunId, rewriteAssistantText, stripLeakedToolSummary, withReplyRunId };
package/dist/plugin2.cjs CHANGED
@@ -8,8 +8,8 @@ let node_url = require("node:url");
8
8
  let node_os = require("node:os");
9
9
  let _alfe_ai_chat = require("@alfe.ai/chat");
10
10
  let _alfe_ai_agent_api_client = require("@alfe.ai/agent-api-client");
11
- let _alfe_ai_openclaw_plugin_kit = require("@alfe.ai/openclaw-plugin-kit");
12
11
  let _alfe_ai_config = require("@alfe.ai/config");
12
+ let _alfe_ai_openclaw_plugin_kit = require("@alfe.ai/openclaw-plugin-kit");
13
13
  //#region src/outbound-media.ts
14
14
  /**
15
15
  * Outbound media upload — agent → user.
@@ -622,6 +622,21 @@ async function sendViaChat(deps, ctx, mediaUrl, components) {
622
622
  };
623
623
  }
624
624
  /**
625
+ * Send to a server-authorized native identity conversation. The cloud
626
+ * Conversation/Participant rows already exist when this is called; bind the
627
+ * local session to the exact same key before reusing the normal delivery path.
628
+ */
629
+ async function sendIdentityMessageViaChat(deps, target, text) {
630
+ const existing = await deps.getSession(target.conversationId);
631
+ if (existing) {
632
+ if (existing.userId !== target.userId || existing.tenantId !== target.tenantId) throw new Error("identity_direct_session_mismatch");
633
+ } else await deps.createSession(target.conversationId, "", "alfe", target.tenantId, target.userId);
634
+ return sendViaChat(deps, {
635
+ to: `conv:${target.conversationId}`,
636
+ text
637
+ }, void 0);
638
+ }
639
+ /**
625
640
  * Present interactive components (link buttons, quick replies) into a
626
641
  * conversation — the outbound path for the `chat_present_components` tool.
627
642
  * `to` is a `conv:{id}` or `user:{id}` target (same vocabulary as the channel's
@@ -1654,8 +1669,92 @@ async function collectTranscriptActivity(conversationId, routes) {
1654
1669
  return merged.length > MAX_ENTRIES ? merged.slice(merged.length - MAX_ENTRIES) : merged;
1655
1670
  }
1656
1671
  //#endregion
1672
+ //#region src/session-wire-budget.ts
1673
+ /**
1674
+ * Keep daemon -> chat-relay history responses below the relay's WebSocket
1675
+ * frame limit. `sessions.get` is assembled on the daemon, so relay-side
1676
+ * activity thinning happens too late to protect this inbound hop.
1677
+ */
1678
+ /**
1679
+ * The production relay historically accepted 1 MiB daemon frames. Leave
1680
+ * enough headroom for WebSocket/RPC envelope drift and non-history metadata.
1681
+ */
1682
+ const SESSION_GET_FRAME_BUDGET_BYTES = 900 * 1024;
1683
+ const SESSION_GET_FRAME_BUDGET_ERROR = "sessions.get response metadata exceeds frame budget";
1684
+ function responseFrameBytes(requestId, payload) {
1685
+ return Buffer.byteLength(JSON.stringify({
1686
+ id: requestId,
1687
+ ok: true,
1688
+ payload
1689
+ }), "utf8");
1690
+ }
1691
+ function largestFittingSuffix(items, fits) {
1692
+ let low = 0;
1693
+ let high = items.length;
1694
+ while (low < high) {
1695
+ const keep = Math.ceil((low + high) / 2);
1696
+ if (fits(items.slice(items.length - keep))) low = keep;
1697
+ else high = keep - 1;
1698
+ }
1699
+ return items.slice(items.length - low);
1700
+ }
1701
+ /**
1702
+ * Preserve the newest useful history without ever letting one oversized
1703
+ * `sessions.get` response tear down the daemon's shared relay connection.
1704
+ * Activity is shed before messages; if messages alone exceed the budget,
1705
+ * their oldest entries are shed as well. Persistence is untouched.
1706
+ */
1707
+ function boundSessionGetResponse(requestId, payload, maxFrameBytes = SESSION_GET_FRAME_BUDGET_BYTES) {
1708
+ const originalFrameBytes = responseFrameBytes(requestId, payload);
1709
+ if (originalFrameBytes <= maxFrameBytes) return {
1710
+ payload,
1711
+ frameBytes: originalFrameBytes,
1712
+ truncatedMessages: 0,
1713
+ truncatedActivity: 0
1714
+ };
1715
+ const originalActivity = payload.activity;
1716
+ let activity = originalActivity;
1717
+ if (originalActivity) activity = largestFittingSuffix(originalActivity, (suffix) => responseFrameBytes(requestId, {
1718
+ ...payload,
1719
+ activity: suffix
1720
+ }) <= maxFrameBytes);
1721
+ let candidate = {
1722
+ ...payload,
1723
+ ...originalActivity ? { activity } : {}
1724
+ };
1725
+ let frameBytes = responseFrameBytes(requestId, candidate);
1726
+ if (frameBytes <= maxFrameBytes) return {
1727
+ payload: candidate,
1728
+ frameBytes,
1729
+ truncatedMessages: 0,
1730
+ truncatedActivity: (originalActivity?.length ?? 0) - (activity?.length ?? 0)
1731
+ };
1732
+ candidate = {
1733
+ ...payload,
1734
+ messages: payload.messages,
1735
+ ...originalActivity ? { activity: [] } : {}
1736
+ };
1737
+ const messages = largestFittingSuffix(payload.messages, (suffix) => responseFrameBytes(requestId, {
1738
+ ...candidate,
1739
+ messages: suffix
1740
+ }) <= maxFrameBytes);
1741
+ candidate = {
1742
+ ...candidate,
1743
+ messages
1744
+ };
1745
+ frameBytes = responseFrameBytes(requestId, candidate);
1746
+ if (frameBytes > maxFrameBytes) throw new Error(SESSION_GET_FRAME_BUDGET_ERROR);
1747
+ return {
1748
+ payload: candidate,
1749
+ frameBytes,
1750
+ truncatedMessages: payload.messages.length - messages.length,
1751
+ truncatedActivity: originalActivity?.length ?? 0
1752
+ };
1753
+ }
1754
+ //#endregion
1657
1755
  //#region src/a2a-tools.ts
1658
1756
  const MAX_AGENT_ID_CHARS = 256;
1757
+ const MAX_IDENTITY_ID_CHARS$1 = 256;
1659
1758
  const MAX_A2A_MESSAGE_CHARS = 32e3;
1660
1759
  const MAX_THREAD_ID_CHARS = 128;
1661
1760
  const MAX_A2A_DEPTH = 20;
@@ -1692,7 +1791,7 @@ function disarmA2AEndSignal() {
1692
1791
  function isA2AEndSignalled() {
1693
1792
  return conversationEndRequested;
1694
1793
  }
1695
- function buildA2ATools(getChatClient, log, present) {
1794
+ function buildA2ATools(getChatClient, log, present, messageIdentity) {
1696
1795
  const requireClient = () => {
1697
1796
  const client = getChatClient();
1698
1797
  if (!client) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)("chat service not connected yet — try again in a moment");
@@ -1784,6 +1883,39 @@ function buildA2ATools(getChatClient, log, present) {
1784
1883
  }
1785
1884
  })
1786
1885
  ];
1886
+ if (messageIdentity) tools.push((0, _alfe_ai_openclaw_plugin_kit.defineTool)({
1887
+ name: "message_identity",
1888
+ description: "Send a new direct message to a person by universal identity ID. Use list_identities or lookup_identity first. The chat service verifies that the identity maps to exactly one current member of this tenant, creates or reuses the durable direct conversation, and then sends the message.",
1889
+ parameters: {
1890
+ type: "object",
1891
+ properties: {
1892
+ identity_id: {
1893
+ type: "string",
1894
+ minLength: 5,
1895
+ maxLength: MAX_IDENTITY_ID_CHARS$1,
1896
+ pattern: "^idn_[A-Za-z0-9_-]+$",
1897
+ description: "Target universal identity ID (idn_...)"
1898
+ },
1899
+ message: {
1900
+ type: "string",
1901
+ minLength: 1,
1902
+ maxLength: MAX_A2A_MESSAGE_CHARS,
1903
+ description: "Message to send to the person"
1904
+ }
1905
+ },
1906
+ required: ["identity_id", "message"]
1907
+ },
1908
+ handler: async (params) => {
1909
+ const identityId = params.identity_id;
1910
+ const message = params.message;
1911
+ if (typeof identityId !== "string" || identityId.length > MAX_IDENTITY_ID_CHARS$1 || !/^idn_[A-Za-z0-9_-]+$/.test(identityId) || typeof message !== "string" || message.length < 1 || message.length > MAX_A2A_MESSAGE_CHARS) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)("Invalid message_identity parameters");
1912
+ log.info(`message_identity tool: sending to ${identityId}`);
1913
+ return messageIdentity({
1914
+ identityId,
1915
+ message
1916
+ });
1917
+ }
1918
+ }));
1787
1919
  if (present) tools.push((0, _alfe_ai_openclaw_plugin_kit.defineTool)({
1788
1920
  name: "chat_present_components",
1789
1921
  description: "Attach interactive UI components to a chat message so the user can act with a tap instead of typing. PREFER components whenever you offer the user a discrete set of choices — a quick_reply or select reads far better than \"reply 1, 2, or 3\". Use:\n • quick_reply — one-tap canned replies for a short set of options.\n • select — a single choice from a longer list (dropdown).\n • multi_select — let the user pick MANY options, then submit.\n • confirm — an approval / yes-no decision (primary + secondary button).\n • link_button — an actionable LINK (open a dashboard, a browser-takeover deep link). URL must be https on an Alfe-owned host.\n • copy_button — let the user copy a value (token, id, snippet) to their clipboard.\nChoosing/submitting an interactive component sends the chosen value back AS THE USER'S NEXT MESSAGE, so the conversation continues normally — you will see it as ordinary user input. Don't button-spam: components are for real interactions, not decoration, and each message allows at most 10. Components degrade to plain text on clients that don't render them, so keep the essentials in `text` too.\nExamples:\n Approval: { to:\"conv:abc\", text:\"Deploy to production?\", components:[ { type:\"confirm\", id:\"c1\", confirmLabel:\"Deploy\", confirmValue:\"yes, deploy\", cancelLabel:\"Cancel\", cancelValue:\"no, hold off\" } ] }\n Pick one: { to:\"user:u_123\", text:\"Which environment?\", components:[ { type:\"select\", id:\"s1\", placeholder:\"Choose an environment\", options:[ { label:\"Dev\", value:\"dev\" }, { label:\"Staging\", value:\"staging\" }, { label:\"Prod\", value:\"prod\" } ] } ] }\n Link: { to:\"conv:abc\", text:\"I need you to finish the login step.\", components:[ { type:\"link_button\", id:\"b1\", label:\"Open browser session\", url:\"https://app.alfe.ai/agents/x?tab=browser\", target:\"new-tab\", style:\"primary\" } ] }",
@@ -2109,6 +2241,28 @@ function isValidIdentityId(value) {
2109
2241
  */
2110
2242
  const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
2111
2243
  const pkg = require$1("../package.json");
2244
+ let cachedAgentApi = null;
2245
+ function getAgentApiClient(apiKeyOverride) {
2246
+ let resolved;
2247
+ try {
2248
+ resolved = (0, _alfe_ai_config.resolveConfig)();
2249
+ } catch {}
2250
+ const normalizedOverride = apiKeyOverride?.trim();
2251
+ const apiKey = normalizedOverride ?? resolved?.apiKey;
2252
+ if (!apiKey) throw new Error("Alfe API key is not configured");
2253
+ const apiUrl = normalizedOverride === void 0 ? resolved?.apiUrl ?? (0, _alfe_ai_config.getEndpointFromToken)(apiKey) : (0, _alfe_ai_config.getEndpointFromToken)(apiKey);
2254
+ if (cachedAgentApi?.apiKey === apiKey && cachedAgentApi.apiUrl === apiUrl) return cachedAgentApi.client;
2255
+ const client = new _alfe_ai_agent_api_client.AgentApiClient({
2256
+ apiKey,
2257
+ apiUrl
2258
+ });
2259
+ cachedAgentApi = {
2260
+ apiKey,
2261
+ apiUrl,
2262
+ client
2263
+ };
2264
+ return client;
2265
+ }
2112
2266
  /**
2113
2267
  * Decide whether an agent-event belongs to THIS turn's run and should be
2114
2268
  * forwarded as a delta / activity frame.
@@ -2148,6 +2302,25 @@ function admitAgentEvent(gate, evt) {
2148
2302
  if (gate.sessionKey && evt.sessionKey && evt.sessionKey !== gate.sessionKey) return false;
2149
2303
  return true;
2150
2304
  }
2305
+ /**
2306
+ * Seed OpenClaw's runtime/event id with the chat service request that owns the
2307
+ * turn. Older callers omit the idempotency key and retain a runtime-owned id.
2308
+ */
2309
+ function resolveRequestedRunId(idempotencyKey) {
2310
+ return typeof idempotencyKey === "string" && idempotencyKey.length > 0 && idempotencyKey.length <= 256 ? idempotencyKey : null;
2311
+ }
2312
+ /** Preserve OpenClaw-provided reply options while seeding this exact run id. */
2313
+ function withReplyRunId(params, runId) {
2314
+ const existing = params.replyOptions;
2315
+ const replyOptions = existing && typeof existing === "object" && !Array.isArray(existing) ? existing : {};
2316
+ return {
2317
+ ...params,
2318
+ replyOptions: {
2319
+ ...replyOptions,
2320
+ runId
2321
+ }
2322
+ };
2323
+ }
2151
2324
  function asString(v) {
2152
2325
  return typeof v === "string" && v.length > 0 ? v : void 0;
2153
2326
  }
@@ -2266,8 +2439,9 @@ function buildToolActivity(data) {
2266
2439
  /**
2267
2440
  * Workstream C, item 5: strip OpenClaw's leaked tool-summary console line
2268
2441
  * from assistant `responseText` before it's stored / notified as a chat
2269
- * bubble. OpenClaw upstream composes a `⚠️ 🛠️ … (+N steps) failed` summary
2270
- * into the assistant text for a non-zero-exit tool run; `failed` there is a
2442
+ * bubble. OpenClaw upstream composes either a `⚠️ 🛠️ … (+N steps) failed`
2443
+ * summary or a step-count-free `⚠️ 🛠️ … → … failed` pipeline into the
2444
+ * assistant text for a non-zero-exit tool run; `failed` there is a
2271
2445
  * TOOL exit status, not a chat-level failure, and it now has a proper
2272
2446
  * structured tool row (see buildToolActivity). This is a BOUNDED, clearly
2273
2447
  * scoped strip of that one known pattern — it must not touch normal prose.
@@ -2284,9 +2458,10 @@ function buildToolActivity(data) {
2284
2458
  * intact. Per-line, line-anchored, non-global.
2285
2459
  */
2286
2460
  const LEAKED_TOOL_SUMMARY_LINE = /^\s*(?:⚠️|🛠️).*\(\+\d+\s+steps?\)\s*(?:failed|succeeded|done)\.?\s*$/iu;
2461
+ const LEAKED_TOOL_PIPELINE_LINE = /^\s*⚠️\s*🛠️\s+.+(?:\s+→\s+.+)+\s+(?:failed|succeeded|done)\.?\s*$/iu;
2287
2462
  function stripLeakedToolSummary(text) {
2288
- if (!text.includes("step")) return text;
2289
- return text.split("\n").filter((line) => !LEAKED_TOOL_SUMMARY_LINE.test(line)).join("\n").replace(/\n{3,}/g, "\n\n").trim();
2463
+ if (!text.includes("step") && !(text.includes("⚠️") && text.includes("🛠️") && text.includes("→"))) return text;
2464
+ return text.split("\n").filter((line) => !LEAKED_TOOL_SUMMARY_LINE.test(line) && !LEAKED_TOOL_PIPELINE_LINE.test(line)).join("\n").replace(/\n{3,}/g, "\n\n").trim();
2290
2465
  }
2291
2466
  /**
2292
2467
  * Build the `a2a-complete` notify payload. ALWAYS fired for an A2A turn —
@@ -2711,6 +2886,7 @@ function enqueueAgentTurn(task, opts) {
2711
2886
  run: task,
2712
2887
  coalesceKey: opts?.coalesceKey,
2713
2888
  onSuperseded: opts?.onSuperseded ?? (() => void 0),
2889
+ onStarted: opts?.onStarted ?? (() => void 0),
2714
2890
  resolve,
2715
2891
  reject
2716
2892
  };
@@ -2736,6 +2912,9 @@ async function drainAgentTurns() {
2736
2912
  const entry = agentTurnBacklog.shift();
2737
2913
  if (!entry) break;
2738
2914
  try {
2915
+ try {
2916
+ entry.onStarted();
2917
+ } catch {}
2739
2918
  await entry.run();
2740
2919
  entry.resolve();
2741
2920
  } catch (err) {
@@ -2772,7 +2951,8 @@ function __setActiveRunForTest(ref) {
2772
2951
  * sessionKey that does NOT match the running turn yields no candidates — we
2773
2952
  * never abort another session's run on a shared daemon.
2774
2953
  */
2775
- function resolveAbortTargetKeys(active, abortSessionKey) {
2954
+ function resolveAbortTargetKeys(active, abortSessionKey, abortRunId) {
2955
+ if (abortRunId && (!active?.runId || active.runId !== abortRunId || abortSessionKey !== void 0 && active.requestSessionKey !== abortSessionKey)) return [];
2776
2956
  const keys = [];
2777
2957
  if (active && (!abortSessionKey || active.requestSessionKey === abortSessionKey)) {
2778
2958
  if (active.routeSessionKey) keys.push(active.routeSessionKey);
@@ -2781,20 +2961,21 @@ function resolveAbortTargetKeys(active, abortSessionKey) {
2781
2961
  return keys;
2782
2962
  }
2783
2963
  /**
2784
- * `chat.abort` handler — genuinely cancels the RUNNING turn on the daemon.
2964
+ * `chat.abort` handler — attempts to cancel the RUNNING turn on the daemon.
2785
2965
  *
2786
2966
  * Invoked OUT-OF-BAND (never through the serialised agent-turn queue) so it
2787
2967
  * executes immediately while a turn is mid-flight. Resolves the live run's
2788
2968
  * sessionId from the target session key and fires the run handle's
2789
2969
  * AbortController via `abortEmbeddedAgentRun`. The in-flight dispatch then
2790
2970
  * settles (AbortError), so `handleAgentRequest` sends its RPC response and the
2791
- * chat service seals the UI. When the abort primitives are unavailable (older
2792
- * openclaw), this degrades to a logged no-op: the UI is still sealed by the
2793
- * chat service's optimistic idle broadcast, but the turn completes server-side.
2971
+ * chat service can settle the UI. When the abort primitives are unavailable
2972
+ * (older OpenClaw), this returns `{aborted:false}`. The service must keep the
2973
+ * turn active until its real terminal frame because the response may still be
2974
+ * persisted.
2794
2975
  */
2795
2976
  function handleChatAbort(request, log) {
2796
- const { sessionKey } = request.params;
2797
- const targetKeys = resolveAbortTargetKeys(activeRun, sessionKey);
2977
+ const { sessionKey, runId } = request.params;
2978
+ const targetKeys = resolveAbortTargetKeys(activeRun, sessionKey, runId);
2798
2979
  let aborted = false;
2799
2980
  if (abortEmbeddedAgentRun && resolveActiveRunSessionId) {
2800
2981
  for (const key of targetKeys) {
@@ -2891,7 +3072,7 @@ async function handleAgentRequest(request, log) {
2891
3072
  chatClient?.sendResponse(request.id, false, { message: "OpenClaw SDK not available — cannot dispatch" });
2892
3073
  return;
2893
3074
  }
2894
- const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, origin, displayName, identityId, identityProvider, attachments: rawAttachments, a2a, chatMessageId } = request.params;
3075
+ const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, origin, displayName, identityId, identityProvider, idempotencyKey, attachments: rawAttachments, a2a, chatMessageId } = request.params;
2895
3076
  const isA2A = !!a2a;
2896
3077
  if (!message && !rawAttachments?.length) {
2897
3078
  chatClient?.sendResponse(request.id, false, { message: "Missing message" });
@@ -2902,8 +3083,9 @@ async function handleAgentRequest(request, log) {
2902
3083
  const sessionId = conversationId ?? legacySessionKey;
2903
3084
  if (!await getSession(sessionId)) await createSession(sessionId, "", "alfe", tenantId, userId);
2904
3085
  await addMessage(sessionId, "user", message, userId ?? senderId, displayName ?? senderId, void 0, userAttachmentsToStored(rawAttachments));
3086
+ const requestedRunId = resolveRequestedRunId(idempotencyKey);
2905
3087
  const runGate = {
2906
- runId: null,
3088
+ runId: requestedRunId,
2907
3089
  sessionKey: null
2908
3090
  };
2909
3091
  let lastThinkingLen = 0;
@@ -3064,6 +3246,7 @@ async function handleAgentRequest(request, log) {
3064
3246
  const conversationLabel = conversationType === "group" ? shortConvId ? `[${channelLabel}] Group (${shortConvId})` : `[${channelLabel}] Group` : shortConvId ? `[${channelLabel}] ${userLabel} (${shortConvId})` : `[${channelLabel}] ${userLabel}`;
3065
3247
  let a2aResponseBuffer = "";
3066
3248
  const outboundMediaCtx = createOutboundMediaContext();
3249
+ let visibleDeliveryIndex = 0;
3067
3250
  if (isA2A) resetA2AEndSignal();
3068
3251
  const peer = conversationType === "group" ? {
3069
3252
  kind: "group",
@@ -3087,11 +3270,19 @@ async function handleAgentRequest(request, log) {
3087
3270
  }
3088
3271
  activeRun = {
3089
3272
  requestSessionKey: legacySessionKey,
3090
- routeSessionKey
3273
+ routeSessionKey,
3274
+ ...requestedRunId ? { runId: requestedRunId } : {}
3091
3275
  };
3276
+ const dispatchChannel = requestedRunId ? {
3277
+ ...runtime.channel,
3278
+ reply: {
3279
+ ...runtime.channel.reply,
3280
+ dispatchReplyWithBufferedBlockDispatcher: (params) => runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher(withReplyRunId(params, requestedRunId))
3281
+ }
3282
+ } : runtime.channel;
3092
3283
  const result = await dispatchInbound({
3093
3284
  cfg,
3094
- runtime: { channel: runtime.channel },
3285
+ runtime: { channel: dispatchChannel },
3095
3286
  channel: "alfe",
3096
3287
  channelLabel,
3097
3288
  accountId: "default",
@@ -3129,6 +3320,8 @@ async function handleAgentRequest(request, log) {
3129
3320
  const note = formatMediaFailureNote(media.newFailures);
3130
3321
  responseText = responseText ? `${responseText}\n\n${note}` : note;
3131
3322
  }
3323
+ if (!responseText && media.mediaUrls.length === 0) return;
3324
+ const deliveryIndex = visibleDeliveryIndex++;
3132
3325
  await addMessage(sessionId, "assistant", responseText, void 0, void 0, void 0, assistantMediaToStored(media.attachments));
3133
3326
  if (isA2A) a2aResponseBuffer += (a2aResponseBuffer && responseText ? "\n\n" : "") + responseText;
3134
3327
  chatClient?.notify("agent-message", {
@@ -3136,6 +3329,7 @@ async function handleAgentRequest(request, log) {
3136
3329
  text: responseText,
3137
3330
  sessionKey: runGate.sessionKey ?? legacySessionKey,
3138
3331
  messageId: chatMessageId ?? request.id,
3332
+ deliveryIndex,
3139
3333
  ...media.mediaUrls.length ? {
3140
3334
  mediaUrls: media.mediaUrls,
3141
3335
  attachments: media.attachments
@@ -3235,7 +3429,7 @@ async function handleSessionsGet(request, log) {
3235
3429
  });
3236
3430
  return;
3237
3431
  }
3238
- chatClient?.sendResponse(request.id, true, {
3432
+ const response = boundSessionGetResponse(request.id, {
3239
3433
  sessionId: session.sessionId,
3240
3434
  agentId: session.agentId,
3241
3435
  channel: session.channel,
@@ -3250,6 +3444,8 @@ async function handleSessionsGet(request, log) {
3250
3444
  })),
3251
3445
  activity: await resolveHistoryActivity(session)
3252
3446
  });
3447
+ if (response.truncatedMessages > 0 || response.truncatedActivity > 0) log.warn(`sessions.get history bounded for relay frame: messagesDropped=${String(response.truncatedMessages)} activityDropped=${String(response.truncatedActivity)} frameBytes=${String(response.frameBytes)}`);
3448
+ chatClient?.sendResponse(request.id, true, response.payload);
3253
3449
  } catch (err) {
3254
3450
  const errMsg = err instanceof Error ? err.message : String(err);
3255
3451
  log.error(`sessions.get failed: ${errMsg}`);
@@ -3263,6 +3459,7 @@ const plugin = {
3263
3459
  version: pkg.version,
3264
3460
  activate(api) {
3265
3461
  const log = api.logger;
3462
+ const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
3266
3463
  const outboundDeps = {
3267
3464
  getChatClient: () => chatClient,
3268
3465
  log,
@@ -3285,11 +3482,27 @@ const plugin = {
3285
3482
  components: clean
3286
3483
  });
3287
3484
  };
3288
- const a2aTools = buildA2ATools(() => chatClient, log, present);
3485
+ const messageIdentity = async (args) => {
3486
+ if (!chatClient?.isConnected) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)("chat service not connected yet — try again in a moment");
3487
+ const direct = await getAgentApiClient(pluginConfig.apiKey).ensureDirectConversation(args.identityId);
3488
+ const sent = await sendIdentityMessageViaChat(outboundDeps, {
3489
+ conversationId: direct.conversationId,
3490
+ tenantId: direct.tenantId,
3491
+ userId: direct.userId
3492
+ }, args.message);
3493
+ return {
3494
+ status: "sent",
3495
+ identityId: direct.identityId,
3496
+ displayName: direct.displayName,
3497
+ conversationId: sent.conversationId,
3498
+ messageId: sent.messageId,
3499
+ newConversation: direct.created
3500
+ };
3501
+ };
3502
+ const a2aTools = buildA2ATools(() => chatClient, log, present, messageIdentity);
3289
3503
  for (const tool of a2aTools) api.registerTool(tool);
3290
3504
  log.info(`Registered ${String(a2aTools.length)} agent tools`);
3291
3505
  }
3292
- const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
3293
3506
  const startChatService = async () => {
3294
3507
  const action = decideChatServiceStart({
3295
3508
  connectInFlight: connectingPromise !== null,
@@ -3330,16 +3543,28 @@ const plugin = {
3330
3543
  if (request.method === "agent") {
3331
3544
  const params = request.params;
3332
3545
  const coalesceKey = params.coalesce === "replace" && typeof params.sessionKey === "string" ? params.sessionKey : void 0;
3333
- await enqueueAgentTurn(() => handleAgentRequest(request, log), coalesceKey ? {
3334
- coalesceKey,
3335
- onSuperseded: () => {
3336
- log.info(`Agent turn superseded before start (coalesce=replace): ${coalesceKey}`);
3546
+ chatClient?.sendResponse(request.id, true, {
3547
+ status: "accepted",
3548
+ phase: "queued"
3549
+ });
3550
+ await enqueueAgentTurn(() => handleAgentRequest(request, log), {
3551
+ onStarted: () => {
3337
3552
  chatClient?.sendResponse(request.id, true, {
3338
- superseded: true,
3339
- sessionKey: coalesceKey
3553
+ status: "accepted",
3554
+ phase: "started"
3340
3555
  });
3341
- }
3342
- } : void 0);
3556
+ },
3557
+ ...coalesceKey ? {
3558
+ coalesceKey,
3559
+ onSuperseded: () => {
3560
+ log.info(`Agent turn superseded before start (coalesce=replace): ${coalesceKey}`);
3561
+ chatClient?.sendResponse(request.id, true, {
3562
+ superseded: true,
3563
+ sessionKey: coalesceKey
3564
+ });
3565
+ }
3566
+ } : {}
3567
+ });
3343
3568
  } else if (request.method === "chat.abort") handleChatAbort(request, log);
3344
3569
  else if (request.method === "sessions.list") await handleSessionsList(request, log);
3345
3570
  else if (request.method === "sessions.get") await handleSessionsGet(request, log);
@@ -3474,6 +3699,7 @@ const plugin = {
3474
3699
  }
3475
3700
  pluginRuntime = null;
3476
3701
  dispatchInbound = null;
3702
+ cachedAgentApi = null;
3477
3703
  (0, _alfe_ai_openclaw_plugin_kit.resetActivation)(CHAT_ACTIVATION_KEY);
3478
3704
  log.info("Chat plugin deactivated");
3479
3705
  }
@@ -3599,6 +3825,12 @@ Object.defineProperty(exports, "resolveAbortTargetKeys", {
3599
3825
  return resolveAbortTargetKeys;
3600
3826
  }
3601
3827
  });
3828
+ Object.defineProperty(exports, "resolveRequestedRunId", {
3829
+ enumerable: true,
3830
+ get: function() {
3831
+ return resolveRequestedRunId;
3832
+ }
3833
+ });
3602
3834
  Object.defineProperty(exports, "rewriteAssistantText", {
3603
3835
  enumerable: true,
3604
3836
  get: function() {
@@ -3611,3 +3843,9 @@ Object.defineProperty(exports, "stripLeakedToolSummary", {
3611
3843
  return stripLeakedToolSummary;
3612
3844
  }
3613
3845
  });
3846
+ Object.defineProperty(exports, "withReplyRunId", {
3847
+ enumerable: true,
3848
+ get: function() {
3849
+ return withReplyRunId;
3850
+ }
3851
+ });
@@ -1,2 +1,2 @@
1
- import { C as rewriteAssistantText, S as resolveAbortTargetKeys, _ as computeOpenClawSdkAnchors, a as MAX_INBOUND_ATTACHMENT_SIZE, b as plugin, c as __agentTurnQueueForTest, d as admitAgentEvent, f as buildA2ACompletePayload, g as buildTurnEnvelopeContext, h as buildToolActivity, i as CROSS_AGENT_TOOLS_SYSTEM_PROMPT, l as __resetAgentTurnQueueForTest, m as buildOriginEnvelopeContext, n as ActiveRunRef, o as RunEventGate, p as buildAttachmentLocalFilename, r as AnchorEnv, s as VOICE_REPLY_SYSTEM_PROMPT, t as A2A_REPLY_SYSTEM_PROMPT, u as __setActiveRunForTest, v as decideChatServiceStart, w as stripLeakedToolSummary, x as readAttachmentBodyBounded, y as joinAssistantText } from "./plugin.cjs";
2
- export { A2A_REPLY_SYSTEM_PROMPT, ActiveRunRef, AnchorEnv, CROSS_AGENT_TOOLS_SYSTEM_PROMPT, MAX_INBOUND_ATTACHMENT_SIZE, RunEventGate, VOICE_REPLY_SYSTEM_PROMPT, __agentTurnQueueForTest, __resetAgentTurnQueueForTest, __setActiveRunForTest, admitAgentEvent, buildA2ACompletePayload, buildAttachmentLocalFilename, buildOriginEnvelopeContext, buildToolActivity, buildTurnEnvelopeContext, computeOpenClawSdkAnchors, decideChatServiceStart, plugin as default, joinAssistantText, readAttachmentBodyBounded, resolveAbortTargetKeys, rewriteAssistantText, stripLeakedToolSummary };
1
+ import { C as resolveRequestedRunId, E as withReplyRunId, S as resolveAbortTargetKeys, T as stripLeakedToolSummary, _ as computeOpenClawSdkAnchors, a as MAX_INBOUND_ATTACHMENT_SIZE, b as plugin, c as __agentTurnQueueForTest, d as admitAgentEvent, f as buildA2ACompletePayload, g as buildTurnEnvelopeContext, h as buildToolActivity, i as CROSS_AGENT_TOOLS_SYSTEM_PROMPT, l as __resetAgentTurnQueueForTest, m as buildOriginEnvelopeContext, n as ActiveRunRef, o as RunEventGate, p as buildAttachmentLocalFilename, r as AnchorEnv, s as VOICE_REPLY_SYSTEM_PROMPT, t as A2A_REPLY_SYSTEM_PROMPT, u as __setActiveRunForTest, v as decideChatServiceStart, w as rewriteAssistantText, x as readAttachmentBodyBounded, y as joinAssistantText } from "./plugin.cjs";
2
+ export { A2A_REPLY_SYSTEM_PROMPT, ActiveRunRef, AnchorEnv, CROSS_AGENT_TOOLS_SYSTEM_PROMPT, MAX_INBOUND_ATTACHMENT_SIZE, RunEventGate, VOICE_REPLY_SYSTEM_PROMPT, __agentTurnQueueForTest, __resetAgentTurnQueueForTest, __setActiveRunForTest, admitAgentEvent, buildA2ACompletePayload, buildAttachmentLocalFilename, buildOriginEnvelopeContext, buildToolActivity, buildTurnEnvelopeContext, computeOpenClawSdkAnchors, decideChatServiceStart, plugin as default, joinAssistantText, readAttachmentBodyBounded, resolveAbortTargetKeys, resolveRequestedRunId, rewriteAssistantText, stripLeakedToolSummary, withReplyRunId };
package/dist/plugin2.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { C as rewriteAssistantText, S as resolveAbortTargetKeys, _ as computeOpenClawSdkAnchors, a as MAX_INBOUND_ATTACHMENT_SIZE, b as plugin, c as __agentTurnQueueForTest, d as admitAgentEvent, f as buildA2ACompletePayload, g as buildTurnEnvelopeContext, h as buildToolActivity, i as CROSS_AGENT_TOOLS_SYSTEM_PROMPT, l as __resetAgentTurnQueueForTest, m as buildOriginEnvelopeContext, n as ActiveRunRef, o as RunEventGate, p as buildAttachmentLocalFilename, r as AnchorEnv, s as VOICE_REPLY_SYSTEM_PROMPT, t as A2A_REPLY_SYSTEM_PROMPT, u as __setActiveRunForTest, v as decideChatServiceStart, w as stripLeakedToolSummary, x as readAttachmentBodyBounded, y as joinAssistantText } from "./plugin.js";
2
- export { A2A_REPLY_SYSTEM_PROMPT, ActiveRunRef, AnchorEnv, CROSS_AGENT_TOOLS_SYSTEM_PROMPT, MAX_INBOUND_ATTACHMENT_SIZE, RunEventGate, VOICE_REPLY_SYSTEM_PROMPT, __agentTurnQueueForTest, __resetAgentTurnQueueForTest, __setActiveRunForTest, admitAgentEvent, buildA2ACompletePayload, buildAttachmentLocalFilename, buildOriginEnvelopeContext, buildToolActivity, buildTurnEnvelopeContext, computeOpenClawSdkAnchors, decideChatServiceStart, plugin as default, joinAssistantText, readAttachmentBodyBounded, resolveAbortTargetKeys, rewriteAssistantText, stripLeakedToolSummary };
1
+ import { C as resolveRequestedRunId, E as withReplyRunId, S as resolveAbortTargetKeys, T as stripLeakedToolSummary, _ as computeOpenClawSdkAnchors, a as MAX_INBOUND_ATTACHMENT_SIZE, b as plugin, c as __agentTurnQueueForTest, d as admitAgentEvent, f as buildA2ACompletePayload, g as buildTurnEnvelopeContext, h as buildToolActivity, i as CROSS_AGENT_TOOLS_SYSTEM_PROMPT, l as __resetAgentTurnQueueForTest, m as buildOriginEnvelopeContext, n as ActiveRunRef, o as RunEventGate, p as buildAttachmentLocalFilename, r as AnchorEnv, s as VOICE_REPLY_SYSTEM_PROMPT, t as A2A_REPLY_SYSTEM_PROMPT, u as __setActiveRunForTest, v as decideChatServiceStart, w as rewriteAssistantText, x as readAttachmentBodyBounded, y as joinAssistantText } from "./plugin.js";
2
+ export { A2A_REPLY_SYSTEM_PROMPT, ActiveRunRef, AnchorEnv, CROSS_AGENT_TOOLS_SYSTEM_PROMPT, MAX_INBOUND_ATTACHMENT_SIZE, RunEventGate, VOICE_REPLY_SYSTEM_PROMPT, __agentTurnQueueForTest, __resetAgentTurnQueueForTest, __setActiveRunForTest, admitAgentEvent, buildA2ACompletePayload, buildAttachmentLocalFilename, buildOriginEnvelopeContext, buildToolActivity, buildTurnEnvelopeContext, computeOpenClawSdkAnchors, decideChatServiceStart, plugin as default, joinAssistantText, readAttachmentBodyBounded, resolveAbortTargetKeys, resolveRequestedRunId, rewriteAssistantText, stripLeakedToolSummary, withReplyRunId };
package/dist/plugin2.js CHANGED
@@ -8,8 +8,8 @@ import { pathToFileURL } from "node:url";
8
8
  import { homedir } from "node:os";
9
9
  import { ChatServiceClient, resolveAlfeChat } from "@alfe.ai/chat";
10
10
  import { AgentApiClient, installToolErrorCapture } from "@alfe.ai/agent-api-client";
11
+ import { getEndpointFromToken, resolveConfig } from "@alfe.ai/config";
11
12
  import { defineTool, getActivationKey, publicToolError, resetActivation } from "@alfe.ai/openclaw-plugin-kit";
12
- import { resolveConfig } from "@alfe.ai/config";
13
13
  //#region src/outbound-media.ts
14
14
  /**
15
15
  * Outbound media upload — agent → user.
@@ -622,6 +622,21 @@ async function sendViaChat(deps, ctx, mediaUrl, components) {
622
622
  };
623
623
  }
624
624
  /**
625
+ * Send to a server-authorized native identity conversation. The cloud
626
+ * Conversation/Participant rows already exist when this is called; bind the
627
+ * local session to the exact same key before reusing the normal delivery path.
628
+ */
629
+ async function sendIdentityMessageViaChat(deps, target, text) {
630
+ const existing = await deps.getSession(target.conversationId);
631
+ if (existing) {
632
+ if (existing.userId !== target.userId || existing.tenantId !== target.tenantId) throw new Error("identity_direct_session_mismatch");
633
+ } else await deps.createSession(target.conversationId, "", "alfe", target.tenantId, target.userId);
634
+ return sendViaChat(deps, {
635
+ to: `conv:${target.conversationId}`,
636
+ text
637
+ }, void 0);
638
+ }
639
+ /**
625
640
  * Present interactive components (link buttons, quick replies) into a
626
641
  * conversation — the outbound path for the `chat_present_components` tool.
627
642
  * `to` is a `conv:{id}` or `user:{id}` target (same vocabulary as the channel's
@@ -1654,8 +1669,92 @@ async function collectTranscriptActivity(conversationId, routes) {
1654
1669
  return merged.length > MAX_ENTRIES ? merged.slice(merged.length - MAX_ENTRIES) : merged;
1655
1670
  }
1656
1671
  //#endregion
1672
+ //#region src/session-wire-budget.ts
1673
+ /**
1674
+ * Keep daemon -> chat-relay history responses below the relay's WebSocket
1675
+ * frame limit. `sessions.get` is assembled on the daemon, so relay-side
1676
+ * activity thinning happens too late to protect this inbound hop.
1677
+ */
1678
+ /**
1679
+ * The production relay historically accepted 1 MiB daemon frames. Leave
1680
+ * enough headroom for WebSocket/RPC envelope drift and non-history metadata.
1681
+ */
1682
+ const SESSION_GET_FRAME_BUDGET_BYTES = 900 * 1024;
1683
+ const SESSION_GET_FRAME_BUDGET_ERROR = "sessions.get response metadata exceeds frame budget";
1684
+ function responseFrameBytes(requestId, payload) {
1685
+ return Buffer.byteLength(JSON.stringify({
1686
+ id: requestId,
1687
+ ok: true,
1688
+ payload
1689
+ }), "utf8");
1690
+ }
1691
+ function largestFittingSuffix(items, fits) {
1692
+ let low = 0;
1693
+ let high = items.length;
1694
+ while (low < high) {
1695
+ const keep = Math.ceil((low + high) / 2);
1696
+ if (fits(items.slice(items.length - keep))) low = keep;
1697
+ else high = keep - 1;
1698
+ }
1699
+ return items.slice(items.length - low);
1700
+ }
1701
+ /**
1702
+ * Preserve the newest useful history without ever letting one oversized
1703
+ * `sessions.get` response tear down the daemon's shared relay connection.
1704
+ * Activity is shed before messages; if messages alone exceed the budget,
1705
+ * their oldest entries are shed as well. Persistence is untouched.
1706
+ */
1707
+ function boundSessionGetResponse(requestId, payload, maxFrameBytes = SESSION_GET_FRAME_BUDGET_BYTES) {
1708
+ const originalFrameBytes = responseFrameBytes(requestId, payload);
1709
+ if (originalFrameBytes <= maxFrameBytes) return {
1710
+ payload,
1711
+ frameBytes: originalFrameBytes,
1712
+ truncatedMessages: 0,
1713
+ truncatedActivity: 0
1714
+ };
1715
+ const originalActivity = payload.activity;
1716
+ let activity = originalActivity;
1717
+ if (originalActivity) activity = largestFittingSuffix(originalActivity, (suffix) => responseFrameBytes(requestId, {
1718
+ ...payload,
1719
+ activity: suffix
1720
+ }) <= maxFrameBytes);
1721
+ let candidate = {
1722
+ ...payload,
1723
+ ...originalActivity ? { activity } : {}
1724
+ };
1725
+ let frameBytes = responseFrameBytes(requestId, candidate);
1726
+ if (frameBytes <= maxFrameBytes) return {
1727
+ payload: candidate,
1728
+ frameBytes,
1729
+ truncatedMessages: 0,
1730
+ truncatedActivity: (originalActivity?.length ?? 0) - (activity?.length ?? 0)
1731
+ };
1732
+ candidate = {
1733
+ ...payload,
1734
+ messages: payload.messages,
1735
+ ...originalActivity ? { activity: [] } : {}
1736
+ };
1737
+ const messages = largestFittingSuffix(payload.messages, (suffix) => responseFrameBytes(requestId, {
1738
+ ...candidate,
1739
+ messages: suffix
1740
+ }) <= maxFrameBytes);
1741
+ candidate = {
1742
+ ...candidate,
1743
+ messages
1744
+ };
1745
+ frameBytes = responseFrameBytes(requestId, candidate);
1746
+ if (frameBytes > maxFrameBytes) throw new Error(SESSION_GET_FRAME_BUDGET_ERROR);
1747
+ return {
1748
+ payload: candidate,
1749
+ frameBytes,
1750
+ truncatedMessages: payload.messages.length - messages.length,
1751
+ truncatedActivity: originalActivity?.length ?? 0
1752
+ };
1753
+ }
1754
+ //#endregion
1657
1755
  //#region src/a2a-tools.ts
1658
1756
  const MAX_AGENT_ID_CHARS = 256;
1757
+ const MAX_IDENTITY_ID_CHARS$1 = 256;
1659
1758
  const MAX_A2A_MESSAGE_CHARS = 32e3;
1660
1759
  const MAX_THREAD_ID_CHARS = 128;
1661
1760
  const MAX_A2A_DEPTH = 20;
@@ -1692,7 +1791,7 @@ function disarmA2AEndSignal() {
1692
1791
  function isA2AEndSignalled() {
1693
1792
  return conversationEndRequested;
1694
1793
  }
1695
- function buildA2ATools(getChatClient, log, present) {
1794
+ function buildA2ATools(getChatClient, log, present, messageIdentity) {
1696
1795
  const requireClient = () => {
1697
1796
  const client = getChatClient();
1698
1797
  if (!client) throw publicToolError("chat service not connected yet — try again in a moment");
@@ -1784,6 +1883,39 @@ function buildA2ATools(getChatClient, log, present) {
1784
1883
  }
1785
1884
  })
1786
1885
  ];
1886
+ if (messageIdentity) tools.push(defineTool({
1887
+ name: "message_identity",
1888
+ description: "Send a new direct message to a person by universal identity ID. Use list_identities or lookup_identity first. The chat service verifies that the identity maps to exactly one current member of this tenant, creates or reuses the durable direct conversation, and then sends the message.",
1889
+ parameters: {
1890
+ type: "object",
1891
+ properties: {
1892
+ identity_id: {
1893
+ type: "string",
1894
+ minLength: 5,
1895
+ maxLength: MAX_IDENTITY_ID_CHARS$1,
1896
+ pattern: "^idn_[A-Za-z0-9_-]+$",
1897
+ description: "Target universal identity ID (idn_...)"
1898
+ },
1899
+ message: {
1900
+ type: "string",
1901
+ minLength: 1,
1902
+ maxLength: MAX_A2A_MESSAGE_CHARS,
1903
+ description: "Message to send to the person"
1904
+ }
1905
+ },
1906
+ required: ["identity_id", "message"]
1907
+ },
1908
+ handler: async (params) => {
1909
+ const identityId = params.identity_id;
1910
+ const message = params.message;
1911
+ if (typeof identityId !== "string" || identityId.length > MAX_IDENTITY_ID_CHARS$1 || !/^idn_[A-Za-z0-9_-]+$/.test(identityId) || typeof message !== "string" || message.length < 1 || message.length > MAX_A2A_MESSAGE_CHARS) throw publicToolError("Invalid message_identity parameters");
1912
+ log.info(`message_identity tool: sending to ${identityId}`);
1913
+ return messageIdentity({
1914
+ identityId,
1915
+ message
1916
+ });
1917
+ }
1918
+ }));
1787
1919
  if (present) tools.push(defineTool({
1788
1920
  name: "chat_present_components",
1789
1921
  description: "Attach interactive UI components to a chat message so the user can act with a tap instead of typing. PREFER components whenever you offer the user a discrete set of choices — a quick_reply or select reads far better than \"reply 1, 2, or 3\". Use:\n • quick_reply — one-tap canned replies for a short set of options.\n • select — a single choice from a longer list (dropdown).\n • multi_select — let the user pick MANY options, then submit.\n • confirm — an approval / yes-no decision (primary + secondary button).\n • link_button — an actionable LINK (open a dashboard, a browser-takeover deep link). URL must be https on an Alfe-owned host.\n • copy_button — let the user copy a value (token, id, snippet) to their clipboard.\nChoosing/submitting an interactive component sends the chosen value back AS THE USER'S NEXT MESSAGE, so the conversation continues normally — you will see it as ordinary user input. Don't button-spam: components are for real interactions, not decoration, and each message allows at most 10. Components degrade to plain text on clients that don't render them, so keep the essentials in `text` too.\nExamples:\n Approval: { to:\"conv:abc\", text:\"Deploy to production?\", components:[ { type:\"confirm\", id:\"c1\", confirmLabel:\"Deploy\", confirmValue:\"yes, deploy\", cancelLabel:\"Cancel\", cancelValue:\"no, hold off\" } ] }\n Pick one: { to:\"user:u_123\", text:\"Which environment?\", components:[ { type:\"select\", id:\"s1\", placeholder:\"Choose an environment\", options:[ { label:\"Dev\", value:\"dev\" }, { label:\"Staging\", value:\"staging\" }, { label:\"Prod\", value:\"prod\" } ] } ] }\n Link: { to:\"conv:abc\", text:\"I need you to finish the login step.\", components:[ { type:\"link_button\", id:\"b1\", label:\"Open browser session\", url:\"https://app.alfe.ai/agents/x?tab=browser\", target:\"new-tab\", style:\"primary\" } ] }",
@@ -2109,6 +2241,28 @@ function isValidIdentityId(value) {
2109
2241
  */
2110
2242
  const require = createRequire(import.meta.url);
2111
2243
  const pkg = require("../package.json");
2244
+ let cachedAgentApi = null;
2245
+ function getAgentApiClient(apiKeyOverride) {
2246
+ let resolved;
2247
+ try {
2248
+ resolved = resolveConfig();
2249
+ } catch {}
2250
+ const normalizedOverride = apiKeyOverride?.trim();
2251
+ const apiKey = normalizedOverride ?? resolved?.apiKey;
2252
+ if (!apiKey) throw new Error("Alfe API key is not configured");
2253
+ const apiUrl = normalizedOverride === void 0 ? resolved?.apiUrl ?? getEndpointFromToken(apiKey) : getEndpointFromToken(apiKey);
2254
+ if (cachedAgentApi?.apiKey === apiKey && cachedAgentApi.apiUrl === apiUrl) return cachedAgentApi.client;
2255
+ const client = new AgentApiClient({
2256
+ apiKey,
2257
+ apiUrl
2258
+ });
2259
+ cachedAgentApi = {
2260
+ apiKey,
2261
+ apiUrl,
2262
+ client
2263
+ };
2264
+ return client;
2265
+ }
2112
2266
  /**
2113
2267
  * Decide whether an agent-event belongs to THIS turn's run and should be
2114
2268
  * forwarded as a delta / activity frame.
@@ -2148,6 +2302,25 @@ function admitAgentEvent(gate, evt) {
2148
2302
  if (gate.sessionKey && evt.sessionKey && evt.sessionKey !== gate.sessionKey) return false;
2149
2303
  return true;
2150
2304
  }
2305
+ /**
2306
+ * Seed OpenClaw's runtime/event id with the chat service request that owns the
2307
+ * turn. Older callers omit the idempotency key and retain a runtime-owned id.
2308
+ */
2309
+ function resolveRequestedRunId(idempotencyKey) {
2310
+ return typeof idempotencyKey === "string" && idempotencyKey.length > 0 && idempotencyKey.length <= 256 ? idempotencyKey : null;
2311
+ }
2312
+ /** Preserve OpenClaw-provided reply options while seeding this exact run id. */
2313
+ function withReplyRunId(params, runId) {
2314
+ const existing = params.replyOptions;
2315
+ const replyOptions = existing && typeof existing === "object" && !Array.isArray(existing) ? existing : {};
2316
+ return {
2317
+ ...params,
2318
+ replyOptions: {
2319
+ ...replyOptions,
2320
+ runId
2321
+ }
2322
+ };
2323
+ }
2151
2324
  function asString(v) {
2152
2325
  return typeof v === "string" && v.length > 0 ? v : void 0;
2153
2326
  }
@@ -2266,8 +2439,9 @@ function buildToolActivity(data) {
2266
2439
  /**
2267
2440
  * Workstream C, item 5: strip OpenClaw's leaked tool-summary console line
2268
2441
  * from assistant `responseText` before it's stored / notified as a chat
2269
- * bubble. OpenClaw upstream composes a `⚠️ 🛠️ … (+N steps) failed` summary
2270
- * into the assistant text for a non-zero-exit tool run; `failed` there is a
2442
+ * bubble. OpenClaw upstream composes either a `⚠️ 🛠️ … (+N steps) failed`
2443
+ * summary or a step-count-free `⚠️ 🛠️ … → … failed` pipeline into the
2444
+ * assistant text for a non-zero-exit tool run; `failed` there is a
2271
2445
  * TOOL exit status, not a chat-level failure, and it now has a proper
2272
2446
  * structured tool row (see buildToolActivity). This is a BOUNDED, clearly
2273
2447
  * scoped strip of that one known pattern — it must not touch normal prose.
@@ -2284,9 +2458,10 @@ function buildToolActivity(data) {
2284
2458
  * intact. Per-line, line-anchored, non-global.
2285
2459
  */
2286
2460
  const LEAKED_TOOL_SUMMARY_LINE = /^\s*(?:⚠️|🛠️).*\(\+\d+\s+steps?\)\s*(?:failed|succeeded|done)\.?\s*$/iu;
2461
+ const LEAKED_TOOL_PIPELINE_LINE = /^\s*⚠️\s*🛠️\s+.+(?:\s+→\s+.+)+\s+(?:failed|succeeded|done)\.?\s*$/iu;
2287
2462
  function stripLeakedToolSummary(text) {
2288
- if (!text.includes("step")) return text;
2289
- return text.split("\n").filter((line) => !LEAKED_TOOL_SUMMARY_LINE.test(line)).join("\n").replace(/\n{3,}/g, "\n\n").trim();
2463
+ if (!text.includes("step") && !(text.includes("⚠️") && text.includes("🛠️") && text.includes("→"))) return text;
2464
+ return text.split("\n").filter((line) => !LEAKED_TOOL_SUMMARY_LINE.test(line) && !LEAKED_TOOL_PIPELINE_LINE.test(line)).join("\n").replace(/\n{3,}/g, "\n\n").trim();
2290
2465
  }
2291
2466
  /**
2292
2467
  * Build the `a2a-complete` notify payload. ALWAYS fired for an A2A turn —
@@ -2711,6 +2886,7 @@ function enqueueAgentTurn(task, opts) {
2711
2886
  run: task,
2712
2887
  coalesceKey: opts?.coalesceKey,
2713
2888
  onSuperseded: opts?.onSuperseded ?? (() => void 0),
2889
+ onStarted: opts?.onStarted ?? (() => void 0),
2714
2890
  resolve,
2715
2891
  reject
2716
2892
  };
@@ -2736,6 +2912,9 @@ async function drainAgentTurns() {
2736
2912
  const entry = agentTurnBacklog.shift();
2737
2913
  if (!entry) break;
2738
2914
  try {
2915
+ try {
2916
+ entry.onStarted();
2917
+ } catch {}
2739
2918
  await entry.run();
2740
2919
  entry.resolve();
2741
2920
  } catch (err) {
@@ -2772,7 +2951,8 @@ function __setActiveRunForTest(ref) {
2772
2951
  * sessionKey that does NOT match the running turn yields no candidates — we
2773
2952
  * never abort another session's run on a shared daemon.
2774
2953
  */
2775
- function resolveAbortTargetKeys(active, abortSessionKey) {
2954
+ function resolveAbortTargetKeys(active, abortSessionKey, abortRunId) {
2955
+ if (abortRunId && (!active?.runId || active.runId !== abortRunId || abortSessionKey !== void 0 && active.requestSessionKey !== abortSessionKey)) return [];
2776
2956
  const keys = [];
2777
2957
  if (active && (!abortSessionKey || active.requestSessionKey === abortSessionKey)) {
2778
2958
  if (active.routeSessionKey) keys.push(active.routeSessionKey);
@@ -2781,20 +2961,21 @@ function resolveAbortTargetKeys(active, abortSessionKey) {
2781
2961
  return keys;
2782
2962
  }
2783
2963
  /**
2784
- * `chat.abort` handler — genuinely cancels the RUNNING turn on the daemon.
2964
+ * `chat.abort` handler — attempts to cancel the RUNNING turn on the daemon.
2785
2965
  *
2786
2966
  * Invoked OUT-OF-BAND (never through the serialised agent-turn queue) so it
2787
2967
  * executes immediately while a turn is mid-flight. Resolves the live run's
2788
2968
  * sessionId from the target session key and fires the run handle's
2789
2969
  * AbortController via `abortEmbeddedAgentRun`. The in-flight dispatch then
2790
2970
  * settles (AbortError), so `handleAgentRequest` sends its RPC response and the
2791
- * chat service seals the UI. When the abort primitives are unavailable (older
2792
- * openclaw), this degrades to a logged no-op: the UI is still sealed by the
2793
- * chat service's optimistic idle broadcast, but the turn completes server-side.
2971
+ * chat service can settle the UI. When the abort primitives are unavailable
2972
+ * (older OpenClaw), this returns `{aborted:false}`. The service must keep the
2973
+ * turn active until its real terminal frame because the response may still be
2974
+ * persisted.
2794
2975
  */
2795
2976
  function handleChatAbort(request, log) {
2796
- const { sessionKey } = request.params;
2797
- const targetKeys = resolveAbortTargetKeys(activeRun, sessionKey);
2977
+ const { sessionKey, runId } = request.params;
2978
+ const targetKeys = resolveAbortTargetKeys(activeRun, sessionKey, runId);
2798
2979
  let aborted = false;
2799
2980
  if (abortEmbeddedAgentRun && resolveActiveRunSessionId) {
2800
2981
  for (const key of targetKeys) {
@@ -2891,7 +3072,7 @@ async function handleAgentRequest(request, log) {
2891
3072
  chatClient?.sendResponse(request.id, false, { message: "OpenClaw SDK not available — cannot dispatch" });
2892
3073
  return;
2893
3074
  }
2894
- const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, origin, displayName, identityId, identityProvider, attachments: rawAttachments, a2a, chatMessageId } = request.params;
3075
+ const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, origin, displayName, identityId, identityProvider, idempotencyKey, attachments: rawAttachments, a2a, chatMessageId } = request.params;
2895
3076
  const isA2A = !!a2a;
2896
3077
  if (!message && !rawAttachments?.length) {
2897
3078
  chatClient?.sendResponse(request.id, false, { message: "Missing message" });
@@ -2902,8 +3083,9 @@ async function handleAgentRequest(request, log) {
2902
3083
  const sessionId = conversationId ?? legacySessionKey;
2903
3084
  if (!await getSession(sessionId)) await createSession(sessionId, "", "alfe", tenantId, userId);
2904
3085
  await addMessage(sessionId, "user", message, userId ?? senderId, displayName ?? senderId, void 0, userAttachmentsToStored(rawAttachments));
3086
+ const requestedRunId = resolveRequestedRunId(idempotencyKey);
2905
3087
  const runGate = {
2906
- runId: null,
3088
+ runId: requestedRunId,
2907
3089
  sessionKey: null
2908
3090
  };
2909
3091
  let lastThinkingLen = 0;
@@ -3064,6 +3246,7 @@ async function handleAgentRequest(request, log) {
3064
3246
  const conversationLabel = conversationType === "group" ? shortConvId ? `[${channelLabel}] Group (${shortConvId})` : `[${channelLabel}] Group` : shortConvId ? `[${channelLabel}] ${userLabel} (${shortConvId})` : `[${channelLabel}] ${userLabel}`;
3065
3247
  let a2aResponseBuffer = "";
3066
3248
  const outboundMediaCtx = createOutboundMediaContext();
3249
+ let visibleDeliveryIndex = 0;
3067
3250
  if (isA2A) resetA2AEndSignal();
3068
3251
  const peer = conversationType === "group" ? {
3069
3252
  kind: "group",
@@ -3087,11 +3270,19 @@ async function handleAgentRequest(request, log) {
3087
3270
  }
3088
3271
  activeRun = {
3089
3272
  requestSessionKey: legacySessionKey,
3090
- routeSessionKey
3273
+ routeSessionKey,
3274
+ ...requestedRunId ? { runId: requestedRunId } : {}
3091
3275
  };
3276
+ const dispatchChannel = requestedRunId ? {
3277
+ ...runtime.channel,
3278
+ reply: {
3279
+ ...runtime.channel.reply,
3280
+ dispatchReplyWithBufferedBlockDispatcher: (params) => runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher(withReplyRunId(params, requestedRunId))
3281
+ }
3282
+ } : runtime.channel;
3092
3283
  const result = await dispatchInbound({
3093
3284
  cfg,
3094
- runtime: { channel: runtime.channel },
3285
+ runtime: { channel: dispatchChannel },
3095
3286
  channel: "alfe",
3096
3287
  channelLabel,
3097
3288
  accountId: "default",
@@ -3129,6 +3320,8 @@ async function handleAgentRequest(request, log) {
3129
3320
  const note = formatMediaFailureNote(media.newFailures);
3130
3321
  responseText = responseText ? `${responseText}\n\n${note}` : note;
3131
3322
  }
3323
+ if (!responseText && media.mediaUrls.length === 0) return;
3324
+ const deliveryIndex = visibleDeliveryIndex++;
3132
3325
  await addMessage(sessionId, "assistant", responseText, void 0, void 0, void 0, assistantMediaToStored(media.attachments));
3133
3326
  if (isA2A) a2aResponseBuffer += (a2aResponseBuffer && responseText ? "\n\n" : "") + responseText;
3134
3327
  chatClient?.notify("agent-message", {
@@ -3136,6 +3329,7 @@ async function handleAgentRequest(request, log) {
3136
3329
  text: responseText,
3137
3330
  sessionKey: runGate.sessionKey ?? legacySessionKey,
3138
3331
  messageId: chatMessageId ?? request.id,
3332
+ deliveryIndex,
3139
3333
  ...media.mediaUrls.length ? {
3140
3334
  mediaUrls: media.mediaUrls,
3141
3335
  attachments: media.attachments
@@ -3235,7 +3429,7 @@ async function handleSessionsGet(request, log) {
3235
3429
  });
3236
3430
  return;
3237
3431
  }
3238
- chatClient?.sendResponse(request.id, true, {
3432
+ const response = boundSessionGetResponse(request.id, {
3239
3433
  sessionId: session.sessionId,
3240
3434
  agentId: session.agentId,
3241
3435
  channel: session.channel,
@@ -3250,6 +3444,8 @@ async function handleSessionsGet(request, log) {
3250
3444
  })),
3251
3445
  activity: await resolveHistoryActivity(session)
3252
3446
  });
3447
+ if (response.truncatedMessages > 0 || response.truncatedActivity > 0) log.warn(`sessions.get history bounded for relay frame: messagesDropped=${String(response.truncatedMessages)} activityDropped=${String(response.truncatedActivity)} frameBytes=${String(response.frameBytes)}`);
3448
+ chatClient?.sendResponse(request.id, true, response.payload);
3253
3449
  } catch (err) {
3254
3450
  const errMsg = err instanceof Error ? err.message : String(err);
3255
3451
  log.error(`sessions.get failed: ${errMsg}`);
@@ -3263,6 +3459,7 @@ const plugin = {
3263
3459
  version: pkg.version,
3264
3460
  activate(api) {
3265
3461
  const log = api.logger;
3462
+ const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
3266
3463
  const outboundDeps = {
3267
3464
  getChatClient: () => chatClient,
3268
3465
  log,
@@ -3285,11 +3482,27 @@ const plugin = {
3285
3482
  components: clean
3286
3483
  });
3287
3484
  };
3288
- const a2aTools = buildA2ATools(() => chatClient, log, present);
3485
+ const messageIdentity = async (args) => {
3486
+ if (!chatClient?.isConnected) throw publicToolError("chat service not connected yet — try again in a moment");
3487
+ const direct = await getAgentApiClient(pluginConfig.apiKey).ensureDirectConversation(args.identityId);
3488
+ const sent = await sendIdentityMessageViaChat(outboundDeps, {
3489
+ conversationId: direct.conversationId,
3490
+ tenantId: direct.tenantId,
3491
+ userId: direct.userId
3492
+ }, args.message);
3493
+ return {
3494
+ status: "sent",
3495
+ identityId: direct.identityId,
3496
+ displayName: direct.displayName,
3497
+ conversationId: sent.conversationId,
3498
+ messageId: sent.messageId,
3499
+ newConversation: direct.created
3500
+ };
3501
+ };
3502
+ const a2aTools = buildA2ATools(() => chatClient, log, present, messageIdentity);
3289
3503
  for (const tool of a2aTools) api.registerTool(tool);
3290
3504
  log.info(`Registered ${String(a2aTools.length)} agent tools`);
3291
3505
  }
3292
- const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
3293
3506
  const startChatService = async () => {
3294
3507
  const action = decideChatServiceStart({
3295
3508
  connectInFlight: connectingPromise !== null,
@@ -3330,16 +3543,28 @@ const plugin = {
3330
3543
  if (request.method === "agent") {
3331
3544
  const params = request.params;
3332
3545
  const coalesceKey = params.coalesce === "replace" && typeof params.sessionKey === "string" ? params.sessionKey : void 0;
3333
- await enqueueAgentTurn(() => handleAgentRequest(request, log), coalesceKey ? {
3334
- coalesceKey,
3335
- onSuperseded: () => {
3336
- log.info(`Agent turn superseded before start (coalesce=replace): ${coalesceKey}`);
3546
+ chatClient?.sendResponse(request.id, true, {
3547
+ status: "accepted",
3548
+ phase: "queued"
3549
+ });
3550
+ await enqueueAgentTurn(() => handleAgentRequest(request, log), {
3551
+ onStarted: () => {
3337
3552
  chatClient?.sendResponse(request.id, true, {
3338
- superseded: true,
3339
- sessionKey: coalesceKey
3553
+ status: "accepted",
3554
+ phase: "started"
3340
3555
  });
3341
- }
3342
- } : void 0);
3556
+ },
3557
+ ...coalesceKey ? {
3558
+ coalesceKey,
3559
+ onSuperseded: () => {
3560
+ log.info(`Agent turn superseded before start (coalesce=replace): ${coalesceKey}`);
3561
+ chatClient?.sendResponse(request.id, true, {
3562
+ superseded: true,
3563
+ sessionKey: coalesceKey
3564
+ });
3565
+ }
3566
+ } : {}
3567
+ });
3343
3568
  } else if (request.method === "chat.abort") handleChatAbort(request, log);
3344
3569
  else if (request.method === "sessions.list") await handleSessionsList(request, log);
3345
3570
  else if (request.method === "sessions.get") await handleSessionsGet(request, log);
@@ -3474,9 +3699,10 @@ const plugin = {
3474
3699
  }
3475
3700
  pluginRuntime = null;
3476
3701
  dispatchInbound = null;
3702
+ cachedAgentApi = null;
3477
3703
  resetActivation(CHAT_ACTIVATION_KEY);
3478
3704
  log.info("Chat plugin deactivated");
3479
3705
  }
3480
3706
  };
3481
3707
  //#endregion
3482
- export { createAlfeChannelPlugin as S, plugin as _, __agentTurnQueueForTest as a, rewriteAssistantText as b, admitAgentEvent as c, buildOriginEnvelopeContext as d, buildToolActivity as f, joinAssistantText as g, decideChatServiceStart as h, VOICE_REPLY_SYSTEM_PROMPT as i, buildA2ACompletePayload as l, computeOpenClawSdkAnchors as m, CROSS_AGENT_TOOLS_SYSTEM_PROMPT as n, __resetAgentTurnQueueForTest as o, buildTurnEnvelopeContext as p, MAX_INBOUND_ATTACHMENT_SIZE as r, __setActiveRunForTest as s, A2A_REPLY_SYSTEM_PROMPT as t, buildAttachmentLocalFilename as u, readAttachmentBodyBounded as v, stripLeakedToolSummary as x, resolveAbortTargetKeys as y };
3708
+ export { withReplyRunId as C, stripLeakedToolSummary as S, plugin as _, __agentTurnQueueForTest as a, resolveRequestedRunId as b, admitAgentEvent as c, buildOriginEnvelopeContext as d, buildToolActivity as f, joinAssistantText as g, decideChatServiceStart as h, VOICE_REPLY_SYSTEM_PROMPT as i, buildA2ACompletePayload as l, computeOpenClawSdkAnchors as m, CROSS_AGENT_TOOLS_SYSTEM_PROMPT as n, __resetAgentTurnQueueForTest as o, buildTurnEnvelopeContext as p, MAX_INBOUND_ATTACHMENT_SIZE as r, __setActiveRunForTest as s, A2A_REPLY_SYSTEM_PROMPT as t, buildAttachmentLocalFilename as u, readAttachmentBodyBounded as v, createAlfeChannelPlugin as w, rewriteAssistantText as x, resolveAbortTargetKeys as y };
@@ -26,6 +26,6 @@
26
26
  }
27
27
  },
28
28
  "contracts": {
29
- "tools": ["list_agents", "message_agent", "end_conversation", "chat_present_components"]
29
+ "tools": ["list_agents", "message_agent", "message_identity", "end_conversation", "chat_present_components"]
30
30
  }
31
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/openclaw-chat",
3
- "version": "0.9.13",
3
+ "version": "0.9.15",
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,7 +27,7 @@
27
27
  "openclaw.plugin.json"
28
28
  ],
29
29
  "dependencies": {
30
- "@alfe.ai/agent-api-client": "^0.17.0",
30
+ "@alfe.ai/agent-api-client": "^0.17.1",
31
31
  "@alfe.ai/chat": "^0.0.18",
32
32
  "@alfe.ai/config": "^0.4.1",
33
33
  "@alfe.ai/openclaw-plugin-kit": "0.2.0"