@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 +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin.cjs +2 -0
- package/dist/plugin.d.cts +12 -2
- package/dist/plugin.d.ts +12 -2
- package/dist/plugin.js +2 -2
- package/dist/plugin2.cjs +266 -28
- package/dist/plugin2.d.cts +2 -2
- package/dist/plugin2.d.ts +2 -2
- package/dist/plugin2.js +255 -29
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as
|
|
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
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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`
|
|
2270
|
-
*
|
|
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 —
|
|
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
|
|
2792
|
-
*
|
|
2793
|
-
*
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
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
|
-
|
|
3339
|
-
|
|
3553
|
+
status: "accepted",
|
|
3554
|
+
phase: "started"
|
|
3340
3555
|
});
|
|
3341
|
-
}
|
|
3342
|
-
|
|
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
|
+
});
|
package/dist/plugin2.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { C as
|
|
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
|
|
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`
|
|
2270
|
-
*
|
|
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 —
|
|
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
|
|
2792
|
-
*
|
|
2793
|
-
*
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
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
|
-
|
|
3339
|
-
|
|
3553
|
+
status: "accepted",
|
|
3554
|
+
phase: "started"
|
|
3340
3555
|
});
|
|
3341
|
-
}
|
|
3342
|
-
|
|
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 {
|
|
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 };
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfe.ai/openclaw-chat",
|
|
3
|
-
"version": "0.9.
|
|
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.
|
|
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"
|