@alfe.ai/openclaw-chat 0.9.14 → 0.9.16

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;
@@ -471,6 +472,10 @@ interface RunEventGate {
471
472
  * Isolation is preserved: turns to a daemon are serialised
472
473
  * (`enqueueAgentTurn`) and the listener is torn down before the next turn
473
474
  * dispatches, so the first relevant event locks onto THIS turn's main run.
475
+ * For callers that supply an idempotency key the gate is instead PRE-ARMED
476
+ * with that service run id (see `resolveRequestedRunId` / `withReplyRunId`,
477
+ * which seed the same value as the runtime's own run id), so no lock-on step
478
+ * is needed and only the service-owned run is ever admitted.
474
479
  * Concurrent heartbeat/cron runs and subagent / side-question runs carry a
475
480
  * different `runId` and are excluded — exactly the leakage the gate exists to
476
481
  * prevent. When a run IS control-UI-visible and both keys are present, a
@@ -480,6 +485,13 @@ interface RunEventGate {
480
485
  * pinned against real 2026.6.10 event shapes.
481
486
  */
482
487
  declare function admitAgentEvent(gate: RunEventGate, evt: Pick<AgentEventPayload, 'runId' | 'stream' | 'sessionKey'>): boolean;
488
+ /**
489
+ * Seed OpenClaw's runtime/event id with the chat service request that owns the
490
+ * turn. Older callers omit the idempotency key and retain a runtime-owned id.
491
+ */
492
+ declare function resolveRequestedRunId(idempotencyKey: unknown): string | null;
493
+ /** Preserve OpenClaw-provided reply options while seeding this exact run id. */
494
+ declare function withReplyRunId(params: Record<string, unknown>, runId: string): Record<string, unknown>;
483
495
  interface PluginServiceContext {
484
496
  config: Record<string, unknown>;
485
497
  workspaceDir?: string;
@@ -667,6 +679,7 @@ declare function buildAttachmentLocalFilename(att: Pick<AttachmentRef, 'id' | 'f
667
679
  declare function enqueueAgentTurn(task: () => Promise<void>, opts?: {
668
680
  coalesceKey?: string;
669
681
  onSuperseded?: () => void;
682
+ onStarted?: () => void;
670
683
  }): Promise<void>;
671
684
  /**
672
685
  * Test-only: reset the agent-turn backlog between unit tests. Not used at
@@ -681,6 +694,7 @@ declare const __agentTurnQueueForTest: {
681
694
  interface ActiveRunRef {
682
695
  requestSessionKey: string;
683
696
  routeSessionKey: string | null;
697
+ runId?: string;
684
698
  }
685
699
  /** Test-only: set/clear the active-run ref for handleChatAbort unit tests. */
686
700
  declare function __setActiveRunForTest(ref: ActiveRunRef | null): void;
@@ -692,7 +706,7 @@ declare function __setActiveRunForTest(ref: ActiveRunRef | null): void;
692
706
  * sessionKey that does NOT match the running turn yields no candidates — we
693
707
  * never abort another session's run on a shared daemon.
694
708
  */
695
- declare function resolveAbortTargetKeys(active: ActiveRunRef | null, abortSessionKey: string | undefined): string[];
709
+ declare function resolveAbortTargetKeys(active: ActiveRunRef | null, abortSessionKey: string | undefined, abortRunId?: string): string[];
696
710
  /**
697
711
  * Speech-style reply guidance injected into the agent's SYSTEM PROMPT for a
698
712
  * live voice-call turn (`origin:'voice'`). The user is speaking on a call and
@@ -755,4 +769,4 @@ declare const plugin: {
755
769
  deactivate(api: PluginApi): Promise<void>;
756
770
  };
757
771
  //#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 };
772
+ 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;
@@ -471,6 +472,10 @@ interface RunEventGate {
471
472
  * Isolation is preserved: turns to a daemon are serialised
472
473
  * (`enqueueAgentTurn`) and the listener is torn down before the next turn
473
474
  * dispatches, so the first relevant event locks onto THIS turn's main run.
475
+ * For callers that supply an idempotency key the gate is instead PRE-ARMED
476
+ * with that service run id (see `resolveRequestedRunId` / `withReplyRunId`,
477
+ * which seed the same value as the runtime's own run id), so no lock-on step
478
+ * is needed and only the service-owned run is ever admitted.
474
479
  * Concurrent heartbeat/cron runs and subagent / side-question runs carry a
475
480
  * different `runId` and are excluded — exactly the leakage the gate exists to
476
481
  * prevent. When a run IS control-UI-visible and both keys are present, a
@@ -480,6 +485,13 @@ interface RunEventGate {
480
485
  * pinned against real 2026.6.10 event shapes.
481
486
  */
482
487
  declare function admitAgentEvent(gate: RunEventGate, evt: Pick<AgentEventPayload, 'runId' | 'stream' | 'sessionKey'>): boolean;
488
+ /**
489
+ * Seed OpenClaw's runtime/event id with the chat service request that owns the
490
+ * turn. Older callers omit the idempotency key and retain a runtime-owned id.
491
+ */
492
+ declare function resolveRequestedRunId(idempotencyKey: unknown): string | null;
493
+ /** Preserve OpenClaw-provided reply options while seeding this exact run id. */
494
+ declare function withReplyRunId(params: Record<string, unknown>, runId: string): Record<string, unknown>;
483
495
  interface PluginServiceContext {
484
496
  config: Record<string, unknown>;
485
497
  workspaceDir?: string;
@@ -667,6 +679,7 @@ declare function buildAttachmentLocalFilename(att: Pick<AttachmentRef, 'id' | 'f
667
679
  declare function enqueueAgentTurn(task: () => Promise<void>, opts?: {
668
680
  coalesceKey?: string;
669
681
  onSuperseded?: () => void;
682
+ onStarted?: () => void;
670
683
  }): Promise<void>;
671
684
  /**
672
685
  * Test-only: reset the agent-turn backlog between unit tests. Not used at
@@ -681,6 +694,7 @@ declare const __agentTurnQueueForTest: {
681
694
  interface ActiveRunRef {
682
695
  requestSessionKey: string;
683
696
  routeSessionKey: string | null;
697
+ runId?: string;
684
698
  }
685
699
  /** Test-only: set/clear the active-run ref for handleChatAbort unit tests. */
686
700
  declare function __setActiveRunForTest(ref: ActiveRunRef | null): void;
@@ -692,7 +706,7 @@ declare function __setActiveRunForTest(ref: ActiveRunRef | null): void;
692
706
  * sessionKey that does NOT match the running turn yields no candidates — we
693
707
  * never abort another session's run on a shared daemon.
694
708
  */
695
- declare function resolveAbortTargetKeys(active: ActiveRunRef | null, abortSessionKey: string | undefined): string[];
709
+ declare function resolveAbortTargetKeys(active: ActiveRunRef | null, abortSessionKey: string | undefined, abortRunId?: string): string[];
696
710
  /**
697
711
  * Speech-style reply guidance injected into the agent's SYSTEM PROMPT for a
698
712
  * live voice-call turn (`origin:'voice'`). The user is speaking on a call and
@@ -755,4 +769,4 @@ declare const plugin: {
755
769
  deactivate(api: PluginApi): Promise<void>;
756
770
  };
757
771
  //#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 };
772
+ 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.
@@ -2130,6 +2284,10 @@ const pkg = require$1("../package.json");
2130
2284
  * Isolation is preserved: turns to a daemon are serialised
2131
2285
  * (`enqueueAgentTurn`) and the listener is torn down before the next turn
2132
2286
  * dispatches, so the first relevant event locks onto THIS turn's main run.
2287
+ * For callers that supply an idempotency key the gate is instead PRE-ARMED
2288
+ * with that service run id (see `resolveRequestedRunId` / `withReplyRunId`,
2289
+ * which seed the same value as the runtime's own run id), so no lock-on step
2290
+ * is needed and only the service-owned run is ever admitted.
2133
2291
  * Concurrent heartbeat/cron runs and subagent / side-question runs carry a
2134
2292
  * different `runId` and are excluded — exactly the leakage the gate exists to
2135
2293
  * prevent. When a run IS control-UI-visible and both keys are present, a
@@ -2148,6 +2306,25 @@ function admitAgentEvent(gate, evt) {
2148
2306
  if (gate.sessionKey && evt.sessionKey && evt.sessionKey !== gate.sessionKey) return false;
2149
2307
  return true;
2150
2308
  }
2309
+ /**
2310
+ * Seed OpenClaw's runtime/event id with the chat service request that owns the
2311
+ * turn. Older callers omit the idempotency key and retain a runtime-owned id.
2312
+ */
2313
+ function resolveRequestedRunId(idempotencyKey) {
2314
+ return typeof idempotencyKey === "string" && idempotencyKey.length > 0 && idempotencyKey.length <= 256 ? idempotencyKey : null;
2315
+ }
2316
+ /** Preserve OpenClaw-provided reply options while seeding this exact run id. */
2317
+ function withReplyRunId(params, runId) {
2318
+ const existing = params.replyOptions;
2319
+ const replyOptions = existing && typeof existing === "object" && !Array.isArray(existing) ? existing : {};
2320
+ return {
2321
+ ...params,
2322
+ replyOptions: {
2323
+ ...replyOptions,
2324
+ runId
2325
+ }
2326
+ };
2327
+ }
2151
2328
  function asString(v) {
2152
2329
  return typeof v === "string" && v.length > 0 ? v : void 0;
2153
2330
  }
@@ -2713,6 +2890,7 @@ function enqueueAgentTurn(task, opts) {
2713
2890
  run: task,
2714
2891
  coalesceKey: opts?.coalesceKey,
2715
2892
  onSuperseded: opts?.onSuperseded ?? (() => void 0),
2893
+ onStarted: opts?.onStarted ?? (() => void 0),
2716
2894
  resolve,
2717
2895
  reject
2718
2896
  };
@@ -2738,6 +2916,9 @@ async function drainAgentTurns() {
2738
2916
  const entry = agentTurnBacklog.shift();
2739
2917
  if (!entry) break;
2740
2918
  try {
2919
+ try {
2920
+ entry.onStarted();
2921
+ } catch {}
2741
2922
  await entry.run();
2742
2923
  entry.resolve();
2743
2924
  } catch (err) {
@@ -2774,7 +2955,8 @@ function __setActiveRunForTest(ref) {
2774
2955
  * sessionKey that does NOT match the running turn yields no candidates — we
2775
2956
  * never abort another session's run on a shared daemon.
2776
2957
  */
2777
- function resolveAbortTargetKeys(active, abortSessionKey) {
2958
+ function resolveAbortTargetKeys(active, abortSessionKey, abortRunId) {
2959
+ if (abortRunId && (!active?.runId || active.runId !== abortRunId || abortSessionKey !== void 0 && active.requestSessionKey !== abortSessionKey)) return [];
2778
2960
  const keys = [];
2779
2961
  if (active && (!abortSessionKey || active.requestSessionKey === abortSessionKey)) {
2780
2962
  if (active.routeSessionKey) keys.push(active.routeSessionKey);
@@ -2796,8 +2978,8 @@ function resolveAbortTargetKeys(active, abortSessionKey) {
2796
2978
  * persisted.
2797
2979
  */
2798
2980
  function handleChatAbort(request, log) {
2799
- const { sessionKey } = request.params;
2800
- const targetKeys = resolveAbortTargetKeys(activeRun, sessionKey);
2981
+ const { sessionKey, runId } = request.params;
2982
+ const targetKeys = resolveAbortTargetKeys(activeRun, sessionKey, runId);
2801
2983
  let aborted = false;
2802
2984
  if (abortEmbeddedAgentRun && resolveActiveRunSessionId) {
2803
2985
  for (const key of targetKeys) {
@@ -2894,7 +3076,7 @@ async function handleAgentRequest(request, log) {
2894
3076
  chatClient?.sendResponse(request.id, false, { message: "OpenClaw SDK not available — cannot dispatch" });
2895
3077
  return;
2896
3078
  }
2897
- const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, origin, displayName, identityId, identityProvider, attachments: rawAttachments, a2a, chatMessageId } = request.params;
3079
+ const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, origin, displayName, identityId, identityProvider, idempotencyKey, attachments: rawAttachments, a2a, chatMessageId } = request.params;
2898
3080
  const isA2A = !!a2a;
2899
3081
  if (!message && !rawAttachments?.length) {
2900
3082
  chatClient?.sendResponse(request.id, false, { message: "Missing message" });
@@ -2905,8 +3087,9 @@ async function handleAgentRequest(request, log) {
2905
3087
  const sessionId = conversationId ?? legacySessionKey;
2906
3088
  if (!await getSession(sessionId)) await createSession(sessionId, "", "alfe", tenantId, userId);
2907
3089
  await addMessage(sessionId, "user", message, userId ?? senderId, displayName ?? senderId, void 0, userAttachmentsToStored(rawAttachments));
3090
+ const requestedRunId = resolveRequestedRunId(idempotencyKey);
2908
3091
  const runGate = {
2909
- runId: null,
3092
+ runId: requestedRunId,
2910
3093
  sessionKey: null
2911
3094
  };
2912
3095
  let lastThinkingLen = 0;
@@ -3091,11 +3274,19 @@ async function handleAgentRequest(request, log) {
3091
3274
  }
3092
3275
  activeRun = {
3093
3276
  requestSessionKey: legacySessionKey,
3094
- routeSessionKey
3277
+ routeSessionKey,
3278
+ ...requestedRunId ? { runId: requestedRunId } : {}
3095
3279
  };
3280
+ const dispatchChannel = requestedRunId ? {
3281
+ ...runtime.channel,
3282
+ reply: {
3283
+ ...runtime.channel.reply,
3284
+ dispatchReplyWithBufferedBlockDispatcher: (params) => runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher(withReplyRunId(params, requestedRunId))
3285
+ }
3286
+ } : runtime.channel;
3096
3287
  const result = await dispatchInbound({
3097
3288
  cfg,
3098
- runtime: { channel: runtime.channel },
3289
+ runtime: { channel: dispatchChannel },
3099
3290
  channel: "alfe",
3100
3291
  channelLabel,
3101
3292
  accountId: "default",
@@ -3242,7 +3433,7 @@ async function handleSessionsGet(request, log) {
3242
3433
  });
3243
3434
  return;
3244
3435
  }
3245
- chatClient?.sendResponse(request.id, true, {
3436
+ const response = boundSessionGetResponse(request.id, {
3246
3437
  sessionId: session.sessionId,
3247
3438
  agentId: session.agentId,
3248
3439
  channel: session.channel,
@@ -3257,6 +3448,8 @@ async function handleSessionsGet(request, log) {
3257
3448
  })),
3258
3449
  activity: await resolveHistoryActivity(session)
3259
3450
  });
3451
+ 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)}`);
3452
+ chatClient?.sendResponse(request.id, true, response.payload);
3260
3453
  } catch (err) {
3261
3454
  const errMsg = err instanceof Error ? err.message : String(err);
3262
3455
  log.error(`sessions.get failed: ${errMsg}`);
@@ -3270,6 +3463,7 @@ const plugin = {
3270
3463
  version: pkg.version,
3271
3464
  activate(api) {
3272
3465
  const log = api.logger;
3466
+ const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
3273
3467
  const outboundDeps = {
3274
3468
  getChatClient: () => chatClient,
3275
3469
  log,
@@ -3292,11 +3486,27 @@ const plugin = {
3292
3486
  components: clean
3293
3487
  });
3294
3488
  };
3295
- const a2aTools = buildA2ATools(() => chatClient, log, present);
3489
+ const messageIdentity = async (args) => {
3490
+ if (!chatClient?.isConnected) throw (0, _alfe_ai_openclaw_plugin_kit.publicToolError)("chat service not connected yet — try again in a moment");
3491
+ const direct = await getAgentApiClient(pluginConfig.apiKey).ensureDirectConversation(args.identityId);
3492
+ const sent = await sendIdentityMessageViaChat(outboundDeps, {
3493
+ conversationId: direct.conversationId,
3494
+ tenantId: direct.tenantId,
3495
+ userId: direct.userId
3496
+ }, args.message);
3497
+ return {
3498
+ status: "sent",
3499
+ identityId: direct.identityId,
3500
+ displayName: direct.displayName,
3501
+ conversationId: sent.conversationId,
3502
+ messageId: sent.messageId,
3503
+ newConversation: direct.created
3504
+ };
3505
+ };
3506
+ const a2aTools = buildA2ATools(() => chatClient, log, present, messageIdentity);
3296
3507
  for (const tool of a2aTools) api.registerTool(tool);
3297
3508
  log.info(`Registered ${String(a2aTools.length)} agent tools`);
3298
3509
  }
3299
- const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
3300
3510
  const startChatService = async () => {
3301
3511
  const action = decideChatServiceStart({
3302
3512
  connectInFlight: connectingPromise !== null,
@@ -3337,16 +3547,28 @@ const plugin = {
3337
3547
  if (request.method === "agent") {
3338
3548
  const params = request.params;
3339
3549
  const coalesceKey = params.coalesce === "replace" && typeof params.sessionKey === "string" ? params.sessionKey : void 0;
3340
- await enqueueAgentTurn(() => handleAgentRequest(request, log), coalesceKey ? {
3341
- coalesceKey,
3342
- onSuperseded: () => {
3343
- log.info(`Agent turn superseded before start (coalesce=replace): ${coalesceKey}`);
3550
+ chatClient?.sendResponse(request.id, true, {
3551
+ status: "accepted",
3552
+ phase: "queued"
3553
+ });
3554
+ await enqueueAgentTurn(() => handleAgentRequest(request, log), {
3555
+ onStarted: () => {
3344
3556
  chatClient?.sendResponse(request.id, true, {
3345
- superseded: true,
3346
- sessionKey: coalesceKey
3557
+ status: "accepted",
3558
+ phase: "started"
3347
3559
  });
3348
- }
3349
- } : void 0);
3560
+ },
3561
+ ...coalesceKey ? {
3562
+ coalesceKey,
3563
+ onSuperseded: () => {
3564
+ log.info(`Agent turn superseded before start (coalesce=replace): ${coalesceKey}`);
3565
+ chatClient?.sendResponse(request.id, true, {
3566
+ superseded: true,
3567
+ sessionKey: coalesceKey
3568
+ });
3569
+ }
3570
+ } : {}
3571
+ });
3350
3572
  } else if (request.method === "chat.abort") handleChatAbort(request, log);
3351
3573
  else if (request.method === "sessions.list") await handleSessionsList(request, log);
3352
3574
  else if (request.method === "sessions.get") await handleSessionsGet(request, log);
@@ -3481,6 +3703,7 @@ const plugin = {
3481
3703
  }
3482
3704
  pluginRuntime = null;
3483
3705
  dispatchInbound = null;
3706
+ cachedAgentApi = null;
3484
3707
  (0, _alfe_ai_openclaw_plugin_kit.resetActivation)(CHAT_ACTIVATION_KEY);
3485
3708
  log.info("Chat plugin deactivated");
3486
3709
  }
@@ -3606,6 +3829,12 @@ Object.defineProperty(exports, "resolveAbortTargetKeys", {
3606
3829
  return resolveAbortTargetKeys;
3607
3830
  }
3608
3831
  });
3832
+ Object.defineProperty(exports, "resolveRequestedRunId", {
3833
+ enumerable: true,
3834
+ get: function() {
3835
+ return resolveRequestedRunId;
3836
+ }
3837
+ });
3609
3838
  Object.defineProperty(exports, "rewriteAssistantText", {
3610
3839
  enumerable: true,
3611
3840
  get: function() {
@@ -3618,3 +3847,9 @@ Object.defineProperty(exports, "stripLeakedToolSummary", {
3618
3847
  return stripLeakedToolSummary;
3619
3848
  }
3620
3849
  });
3850
+ Object.defineProperty(exports, "withReplyRunId", {
3851
+ enumerable: true,
3852
+ get: function() {
3853
+ return withReplyRunId;
3854
+ }
3855
+ });
@@ -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.
@@ -2130,6 +2284,10 @@ const pkg = require("../package.json");
2130
2284
  * Isolation is preserved: turns to a daemon are serialised
2131
2285
  * (`enqueueAgentTurn`) and the listener is torn down before the next turn
2132
2286
  * dispatches, so the first relevant event locks onto THIS turn's main run.
2287
+ * For callers that supply an idempotency key the gate is instead PRE-ARMED
2288
+ * with that service run id (see `resolveRequestedRunId` / `withReplyRunId`,
2289
+ * which seed the same value as the runtime's own run id), so no lock-on step
2290
+ * is needed and only the service-owned run is ever admitted.
2133
2291
  * Concurrent heartbeat/cron runs and subagent / side-question runs carry a
2134
2292
  * different `runId` and are excluded — exactly the leakage the gate exists to
2135
2293
  * prevent. When a run IS control-UI-visible and both keys are present, a
@@ -2148,6 +2306,25 @@ function admitAgentEvent(gate, evt) {
2148
2306
  if (gate.sessionKey && evt.sessionKey && evt.sessionKey !== gate.sessionKey) return false;
2149
2307
  return true;
2150
2308
  }
2309
+ /**
2310
+ * Seed OpenClaw's runtime/event id with the chat service request that owns the
2311
+ * turn. Older callers omit the idempotency key and retain a runtime-owned id.
2312
+ */
2313
+ function resolveRequestedRunId(idempotencyKey) {
2314
+ return typeof idempotencyKey === "string" && idempotencyKey.length > 0 && idempotencyKey.length <= 256 ? idempotencyKey : null;
2315
+ }
2316
+ /** Preserve OpenClaw-provided reply options while seeding this exact run id. */
2317
+ function withReplyRunId(params, runId) {
2318
+ const existing = params.replyOptions;
2319
+ const replyOptions = existing && typeof existing === "object" && !Array.isArray(existing) ? existing : {};
2320
+ return {
2321
+ ...params,
2322
+ replyOptions: {
2323
+ ...replyOptions,
2324
+ runId
2325
+ }
2326
+ };
2327
+ }
2151
2328
  function asString(v) {
2152
2329
  return typeof v === "string" && v.length > 0 ? v : void 0;
2153
2330
  }
@@ -2713,6 +2890,7 @@ function enqueueAgentTurn(task, opts) {
2713
2890
  run: task,
2714
2891
  coalesceKey: opts?.coalesceKey,
2715
2892
  onSuperseded: opts?.onSuperseded ?? (() => void 0),
2893
+ onStarted: opts?.onStarted ?? (() => void 0),
2716
2894
  resolve,
2717
2895
  reject
2718
2896
  };
@@ -2738,6 +2916,9 @@ async function drainAgentTurns() {
2738
2916
  const entry = agentTurnBacklog.shift();
2739
2917
  if (!entry) break;
2740
2918
  try {
2919
+ try {
2920
+ entry.onStarted();
2921
+ } catch {}
2741
2922
  await entry.run();
2742
2923
  entry.resolve();
2743
2924
  } catch (err) {
@@ -2774,7 +2955,8 @@ function __setActiveRunForTest(ref) {
2774
2955
  * sessionKey that does NOT match the running turn yields no candidates — we
2775
2956
  * never abort another session's run on a shared daemon.
2776
2957
  */
2777
- function resolveAbortTargetKeys(active, abortSessionKey) {
2958
+ function resolveAbortTargetKeys(active, abortSessionKey, abortRunId) {
2959
+ if (abortRunId && (!active?.runId || active.runId !== abortRunId || abortSessionKey !== void 0 && active.requestSessionKey !== abortSessionKey)) return [];
2778
2960
  const keys = [];
2779
2961
  if (active && (!abortSessionKey || active.requestSessionKey === abortSessionKey)) {
2780
2962
  if (active.routeSessionKey) keys.push(active.routeSessionKey);
@@ -2796,8 +2978,8 @@ function resolveAbortTargetKeys(active, abortSessionKey) {
2796
2978
  * persisted.
2797
2979
  */
2798
2980
  function handleChatAbort(request, log) {
2799
- const { sessionKey } = request.params;
2800
- const targetKeys = resolveAbortTargetKeys(activeRun, sessionKey);
2981
+ const { sessionKey, runId } = request.params;
2982
+ const targetKeys = resolveAbortTargetKeys(activeRun, sessionKey, runId);
2801
2983
  let aborted = false;
2802
2984
  if (abortEmbeddedAgentRun && resolveActiveRunSessionId) {
2803
2985
  for (const key of targetKeys) {
@@ -2894,7 +3076,7 @@ async function handleAgentRequest(request, log) {
2894
3076
  chatClient?.sendResponse(request.id, false, { message: "OpenClaw SDK not available — cannot dispatch" });
2895
3077
  return;
2896
3078
  }
2897
- const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, origin, displayName, identityId, identityProvider, attachments: rawAttachments, a2a, chatMessageId } = request.params;
3079
+ const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, origin, displayName, identityId, identityProvider, idempotencyKey, attachments: rawAttachments, a2a, chatMessageId } = request.params;
2898
3080
  const isA2A = !!a2a;
2899
3081
  if (!message && !rawAttachments?.length) {
2900
3082
  chatClient?.sendResponse(request.id, false, { message: "Missing message" });
@@ -2905,8 +3087,9 @@ async function handleAgentRequest(request, log) {
2905
3087
  const sessionId = conversationId ?? legacySessionKey;
2906
3088
  if (!await getSession(sessionId)) await createSession(sessionId, "", "alfe", tenantId, userId);
2907
3089
  await addMessage(sessionId, "user", message, userId ?? senderId, displayName ?? senderId, void 0, userAttachmentsToStored(rawAttachments));
3090
+ const requestedRunId = resolveRequestedRunId(idempotencyKey);
2908
3091
  const runGate = {
2909
- runId: null,
3092
+ runId: requestedRunId,
2910
3093
  sessionKey: null
2911
3094
  };
2912
3095
  let lastThinkingLen = 0;
@@ -3091,11 +3274,19 @@ async function handleAgentRequest(request, log) {
3091
3274
  }
3092
3275
  activeRun = {
3093
3276
  requestSessionKey: legacySessionKey,
3094
- routeSessionKey
3277
+ routeSessionKey,
3278
+ ...requestedRunId ? { runId: requestedRunId } : {}
3095
3279
  };
3280
+ const dispatchChannel = requestedRunId ? {
3281
+ ...runtime.channel,
3282
+ reply: {
3283
+ ...runtime.channel.reply,
3284
+ dispatchReplyWithBufferedBlockDispatcher: (params) => runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher(withReplyRunId(params, requestedRunId))
3285
+ }
3286
+ } : runtime.channel;
3096
3287
  const result = await dispatchInbound({
3097
3288
  cfg,
3098
- runtime: { channel: runtime.channel },
3289
+ runtime: { channel: dispatchChannel },
3099
3290
  channel: "alfe",
3100
3291
  channelLabel,
3101
3292
  accountId: "default",
@@ -3242,7 +3433,7 @@ async function handleSessionsGet(request, log) {
3242
3433
  });
3243
3434
  return;
3244
3435
  }
3245
- chatClient?.sendResponse(request.id, true, {
3436
+ const response = boundSessionGetResponse(request.id, {
3246
3437
  sessionId: session.sessionId,
3247
3438
  agentId: session.agentId,
3248
3439
  channel: session.channel,
@@ -3257,6 +3448,8 @@ async function handleSessionsGet(request, log) {
3257
3448
  })),
3258
3449
  activity: await resolveHistoryActivity(session)
3259
3450
  });
3451
+ 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)}`);
3452
+ chatClient?.sendResponse(request.id, true, response.payload);
3260
3453
  } catch (err) {
3261
3454
  const errMsg = err instanceof Error ? err.message : String(err);
3262
3455
  log.error(`sessions.get failed: ${errMsg}`);
@@ -3270,6 +3463,7 @@ const plugin = {
3270
3463
  version: pkg.version,
3271
3464
  activate(api) {
3272
3465
  const log = api.logger;
3466
+ const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
3273
3467
  const outboundDeps = {
3274
3468
  getChatClient: () => chatClient,
3275
3469
  log,
@@ -3292,11 +3486,27 @@ const plugin = {
3292
3486
  components: clean
3293
3487
  });
3294
3488
  };
3295
- const a2aTools = buildA2ATools(() => chatClient, log, present);
3489
+ const messageIdentity = async (args) => {
3490
+ if (!chatClient?.isConnected) throw publicToolError("chat service not connected yet — try again in a moment");
3491
+ const direct = await getAgentApiClient(pluginConfig.apiKey).ensureDirectConversation(args.identityId);
3492
+ const sent = await sendIdentityMessageViaChat(outboundDeps, {
3493
+ conversationId: direct.conversationId,
3494
+ tenantId: direct.tenantId,
3495
+ userId: direct.userId
3496
+ }, args.message);
3497
+ return {
3498
+ status: "sent",
3499
+ identityId: direct.identityId,
3500
+ displayName: direct.displayName,
3501
+ conversationId: sent.conversationId,
3502
+ messageId: sent.messageId,
3503
+ newConversation: direct.created
3504
+ };
3505
+ };
3506
+ const a2aTools = buildA2ATools(() => chatClient, log, present, messageIdentity);
3296
3507
  for (const tool of a2aTools) api.registerTool(tool);
3297
3508
  log.info(`Registered ${String(a2aTools.length)} agent tools`);
3298
3509
  }
3299
- const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
3300
3510
  const startChatService = async () => {
3301
3511
  const action = decideChatServiceStart({
3302
3512
  connectInFlight: connectingPromise !== null,
@@ -3337,16 +3547,28 @@ const plugin = {
3337
3547
  if (request.method === "agent") {
3338
3548
  const params = request.params;
3339
3549
  const coalesceKey = params.coalesce === "replace" && typeof params.sessionKey === "string" ? params.sessionKey : void 0;
3340
- await enqueueAgentTurn(() => handleAgentRequest(request, log), coalesceKey ? {
3341
- coalesceKey,
3342
- onSuperseded: () => {
3343
- log.info(`Agent turn superseded before start (coalesce=replace): ${coalesceKey}`);
3550
+ chatClient?.sendResponse(request.id, true, {
3551
+ status: "accepted",
3552
+ phase: "queued"
3553
+ });
3554
+ await enqueueAgentTurn(() => handleAgentRequest(request, log), {
3555
+ onStarted: () => {
3344
3556
  chatClient?.sendResponse(request.id, true, {
3345
- superseded: true,
3346
- sessionKey: coalesceKey
3557
+ status: "accepted",
3558
+ phase: "started"
3347
3559
  });
3348
- }
3349
- } : void 0);
3560
+ },
3561
+ ...coalesceKey ? {
3562
+ coalesceKey,
3563
+ onSuperseded: () => {
3564
+ log.info(`Agent turn superseded before start (coalesce=replace): ${coalesceKey}`);
3565
+ chatClient?.sendResponse(request.id, true, {
3566
+ superseded: true,
3567
+ sessionKey: coalesceKey
3568
+ });
3569
+ }
3570
+ } : {}
3571
+ });
3350
3572
  } else if (request.method === "chat.abort") handleChatAbort(request, log);
3351
3573
  else if (request.method === "sessions.list") await handleSessionsList(request, log);
3352
3574
  else if (request.method === "sessions.get") await handleSessionsGet(request, log);
@@ -3481,9 +3703,10 @@ const plugin = {
3481
3703
  }
3482
3704
  pluginRuntime = null;
3483
3705
  dispatchInbound = null;
3706
+ cachedAgentApi = null;
3484
3707
  resetActivation(CHAT_ACTIVATION_KEY);
3485
3708
  log.info("Chat plugin deactivated");
3486
3709
  }
3487
3710
  };
3488
3711
  //#endregion
3489
- 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 };
3712
+ 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.14",
3
+ "version": "0.9.16",
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,10 +27,10 @@
27
27
  "openclaw.plugin.json"
28
28
  ],
29
29
  "dependencies": {
30
- "@alfe.ai/chat": "^0.0.18",
31
- "@alfe.ai/openclaw-plugin-kit": "0.2.0",
32
- "@alfe.ai/agent-api-client": "^0.17.0",
33
- "@alfe.ai/config": "^0.4.1"
30
+ "@alfe.ai/agent-api-client": "^0.17.1",
31
+ "@alfe.ai/chat": "^0.0.19",
32
+ "@alfe.ai/config": "^0.4.1",
33
+ "@alfe.ai/openclaw-plugin-kit": "0.2.0"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "openclaw": ">=2026.3.0"