@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 +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 +16 -2
- package/dist/plugin.d.ts +16 -2
- package/dist/plugin.js +2 -2
- package/dist/plugin2.cjs +255 -20
- package/dist/plugin2.d.cts +2 -2
- package/dist/plugin2.d.ts +2 -2
- package/dist/plugin2.js +244 -21
- package/openclaw.plugin.json +1 -1
- package/package.json +5 -5
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;
|
|
@@ -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 {
|
|
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 {
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
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
|
-
|
|
3346
|
-
|
|
3557
|
+
status: "accepted",
|
|
3558
|
+
phase: "started"
|
|
3347
3559
|
});
|
|
3348
|
-
}
|
|
3349
|
-
|
|
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
|
+
});
|
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.
|
|
@@ -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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
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
|
-
|
|
3346
|
-
|
|
3557
|
+
status: "accepted",
|
|
3558
|
+
phase: "started"
|
|
3347
3559
|
});
|
|
3348
|
-
}
|
|
3349
|
-
|
|
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 {
|
|
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 };
|
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.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/
|
|
31
|
-
"@alfe.ai/
|
|
32
|
-
"@alfe.ai/
|
|
33
|
-
"@alfe.ai/
|
|
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"
|