@alfe.ai/openclaw-chat 0.9.4 → 0.9.6
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 +1 -0
- package/dist/plugin.d.cts +56 -1
- package/dist/plugin.d.ts +56 -1
- package/dist/plugin.js +2 -2
- package/dist/plugin2.cjs +193 -33
- package/dist/plugin2.d.cts +2 -2
- package/dist/plugin2.d.ts +2 -2
- package/dist/plugin2.js +188 -34
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { C as
|
|
1
|
+
import { C as AlfeResolvedAccount, E as SessionSummary, S as AlfePluginConfig, T as SessionData, b as createAlfeChannelPlugin, g as plugin, w as ChatMessage, x as AlfeChannelConfig } 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 { C as
|
|
1
|
+
import { C as AlfeResolvedAccount, E as SessionSummary, S as AlfePluginConfig, T as SessionData, b as createAlfeChannelPlugin, g as plugin, w as ChatMessage, x as AlfeChannelConfig } 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 createAlfeChannelPlugin, p as plugin } from "./plugin2.js";
|
|
2
2
|
export { createAlfeChannelPlugin, plugin as default };
|
package/dist/plugin.cjs
CHANGED
|
@@ -13,6 +13,7 @@ exports.buildA2ACompletePayload = require_plugin.buildA2ACompletePayload;
|
|
|
13
13
|
exports.buildOriginEnvelopeContext = require_plugin.buildOriginEnvelopeContext;
|
|
14
14
|
exports.buildToolActivity = require_plugin.buildToolActivity;
|
|
15
15
|
exports.computeOpenClawSdkAnchors = require_plugin.computeOpenClawSdkAnchors;
|
|
16
|
+
exports.decideChatServiceStart = require_plugin.decideChatServiceStart;
|
|
16
17
|
exports.default = require_plugin.plugin;
|
|
17
18
|
exports.joinAssistantText = require_plugin.joinAssistantText;
|
|
18
19
|
exports.resolveAbortTargetKeys = require_plugin.resolveAbortTargetKeys;
|
package/dist/plugin.d.cts
CHANGED
|
@@ -64,6 +64,20 @@ type StoredMessageComponent = {
|
|
|
64
64
|
label: string;
|
|
65
65
|
value: string;
|
|
66
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* Persisted reference to a message attachment. Deliberately stores NO URL —
|
|
69
|
+
* the download URL is short-lived and re-signed on demand through the chat
|
|
70
|
+
* service's participant-gated resolve endpoint (keyed by `attachmentId`). Only
|
|
71
|
+
* media bound to the conversation (a real chat-bucket upload) is persisted;
|
|
72
|
+
* remote-passthrough media has no `attachmentId` and is not stored.
|
|
73
|
+
*/
|
|
74
|
+
interface StoredAttachment {
|
|
75
|
+
attachmentId: string;
|
|
76
|
+
type: 'image' | 'video' | 'audio' | 'document' | 'file';
|
|
77
|
+
mimeType: string;
|
|
78
|
+
filename: string;
|
|
79
|
+
size?: number;
|
|
80
|
+
}
|
|
67
81
|
interface ChatMessage {
|
|
68
82
|
role: 'user' | 'assistant';
|
|
69
83
|
content: string;
|
|
@@ -77,6 +91,12 @@ interface ChatMessage {
|
|
|
77
91
|
* content). Persisted so a refresh / reconnect replays the button.
|
|
78
92
|
*/
|
|
79
93
|
components?: StoredMessageComponent[];
|
|
94
|
+
/**
|
|
95
|
+
* Attachment references (images/files). Additive — absent on pre-attachment
|
|
96
|
+
* files. Persisted so history replays them; the client resolves each URL on
|
|
97
|
+
* demand via the participant-gated endpoint. See {@link StoredAttachment}.
|
|
98
|
+
*/
|
|
99
|
+
attachments?: StoredAttachment[];
|
|
80
100
|
}
|
|
81
101
|
/**
|
|
82
102
|
* Persisted form of a turn's agent activity (tool cards + thinking), so a
|
|
@@ -579,6 +599,15 @@ interface AnchorEnv {
|
|
|
579
599
|
requireMainFilename: string | undefined;
|
|
580
600
|
argv1: string | undefined;
|
|
581
601
|
execPath: string;
|
|
602
|
+
/**
|
|
603
|
+
* Well-known global `node_modules` directories to try as `<dir>/openclaw`
|
|
604
|
+
* anchors, independent of PATH / `which` / execPath. Load-bearing when the
|
|
605
|
+
* plugin runs in-process inside a runtime whose PATH is stripped or whose
|
|
606
|
+
* `child_process` is sandboxed (homebrew-keg self-hosted macOS), so every
|
|
607
|
+
* environment-derived anchor above returns nothing. Optional — defaults to
|
|
608
|
+
* none (tests that don't exercise this pass `undefined`).
|
|
609
|
+
*/
|
|
610
|
+
globalModulesDirs?: string[];
|
|
582
611
|
}
|
|
583
612
|
/**
|
|
584
613
|
* Compute the ordered, de-duplicated list of resolver anchors to try, each
|
|
@@ -600,6 +629,32 @@ interface AnchorEnv {
|
|
|
600
629
|
* Pure except for the injected `realpath` / `which` seams — unit-tested.
|
|
601
630
|
*/
|
|
602
631
|
declare function computeOpenClawSdkAnchors(deps: AnchorEnv): string[];
|
|
632
|
+
/**
|
|
633
|
+
* Decide what `startChatService()` should do given the current connection
|
|
634
|
+
* state. Pure so the branch logic — the load-bearing fix for the
|
|
635
|
+
* health-monitor no-op restart loop (Badi Junior, 2026-07-09) — is unit
|
|
636
|
+
* testable without standing up a real WebSocket or the OpenClaw SDK.
|
|
637
|
+
*
|
|
638
|
+
* OpenClaw calls the registered service's `start()` in two very different
|
|
639
|
+
* situations that a sticky activation flag could not tell apart:
|
|
640
|
+
* - a redundant duplicate (re-`activate()` per tool-discovery pass / reconnect)
|
|
641
|
+
* → must be a no-op ('skip'), and
|
|
642
|
+
* - a channel health-monitor REVIVE of a channel it decided is "stopped"
|
|
643
|
+
* (`health-monitor: [alfe:default] restarting (reason: stopped)`), sometimes
|
|
644
|
+
* with no preceding `stop()` → must actually reconnect.
|
|
645
|
+
*
|
|
646
|
+
* Keying on the live client fixes it:
|
|
647
|
+
* - a connect in flight, or an already-connected client → 'skip'
|
|
648
|
+
* - a client that exists but is not connected → 'recycle' it then reconnect
|
|
649
|
+
* - no client at all → 'connect'
|
|
650
|
+
*
|
|
651
|
+
* Exported for unit tests.
|
|
652
|
+
*/
|
|
653
|
+
declare function decideChatServiceStart(state: {
|
|
654
|
+
connectInFlight: boolean;
|
|
655
|
+
clientPresent: boolean;
|
|
656
|
+
clientConnected: boolean;
|
|
657
|
+
}): 'skip' | 'recycle' | 'connect';
|
|
603
658
|
declare function enqueueAgentTurn(task: () => Promise<void>, opts?: {
|
|
604
659
|
coalesceKey?: string;
|
|
605
660
|
onSuperseded?: () => void;
|
|
@@ -681,4 +736,4 @@ declare const plugin: {
|
|
|
681
736
|
deactivate(api: PluginApi): Promise<void>;
|
|
682
737
|
};
|
|
683
738
|
//#endregion
|
|
684
|
-
export {
|
|
739
|
+
export { AlfeResolvedAccount as C, SessionSummary as E, AlfePluginConfig as S, SessionData as T, resolveAbortTargetKeys as _, VOICE_REPLY_SYSTEM_PROMPT as a, createAlfeChannelPlugin as b, __setActiveRunForTest as c, buildOriginEnvelopeContext as d, buildToolActivity as f, plugin as g, joinAssistantText as h, RunEventGate as i, admitAgentEvent as l, decideChatServiceStart as m, AnchorEnv as n, __agentTurnQueueForTest as o, computeOpenClawSdkAnchors as p, CROSS_AGENT_TOOLS_SYSTEM_PROMPT as r, __resetAgentTurnQueueForTest as s, ActiveRunRef as t, buildA2ACompletePayload as u, rewriteAssistantText as v, ChatMessage as w, AlfeChannelConfig as x, stripLeakedToolSummary as y };
|
package/dist/plugin.d.ts
CHANGED
|
@@ -64,6 +64,20 @@ type StoredMessageComponent = {
|
|
|
64
64
|
label: string;
|
|
65
65
|
value: string;
|
|
66
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* Persisted reference to a message attachment. Deliberately stores NO URL —
|
|
69
|
+
* the download URL is short-lived and re-signed on demand through the chat
|
|
70
|
+
* service's participant-gated resolve endpoint (keyed by `attachmentId`). Only
|
|
71
|
+
* media bound to the conversation (a real chat-bucket upload) is persisted;
|
|
72
|
+
* remote-passthrough media has no `attachmentId` and is not stored.
|
|
73
|
+
*/
|
|
74
|
+
interface StoredAttachment {
|
|
75
|
+
attachmentId: string;
|
|
76
|
+
type: 'image' | 'video' | 'audio' | 'document' | 'file';
|
|
77
|
+
mimeType: string;
|
|
78
|
+
filename: string;
|
|
79
|
+
size?: number;
|
|
80
|
+
}
|
|
67
81
|
interface ChatMessage {
|
|
68
82
|
role: 'user' | 'assistant';
|
|
69
83
|
content: string;
|
|
@@ -77,6 +91,12 @@ interface ChatMessage {
|
|
|
77
91
|
* content). Persisted so a refresh / reconnect replays the button.
|
|
78
92
|
*/
|
|
79
93
|
components?: StoredMessageComponent[];
|
|
94
|
+
/**
|
|
95
|
+
* Attachment references (images/files). Additive — absent on pre-attachment
|
|
96
|
+
* files. Persisted so history replays them; the client resolves each URL on
|
|
97
|
+
* demand via the participant-gated endpoint. See {@link StoredAttachment}.
|
|
98
|
+
*/
|
|
99
|
+
attachments?: StoredAttachment[];
|
|
80
100
|
}
|
|
81
101
|
/**
|
|
82
102
|
* Persisted form of a turn's agent activity (tool cards + thinking), so a
|
|
@@ -579,6 +599,15 @@ interface AnchorEnv {
|
|
|
579
599
|
requireMainFilename: string | undefined;
|
|
580
600
|
argv1: string | undefined;
|
|
581
601
|
execPath: string;
|
|
602
|
+
/**
|
|
603
|
+
* Well-known global `node_modules` directories to try as `<dir>/openclaw`
|
|
604
|
+
* anchors, independent of PATH / `which` / execPath. Load-bearing when the
|
|
605
|
+
* plugin runs in-process inside a runtime whose PATH is stripped or whose
|
|
606
|
+
* `child_process` is sandboxed (homebrew-keg self-hosted macOS), so every
|
|
607
|
+
* environment-derived anchor above returns nothing. Optional — defaults to
|
|
608
|
+
* none (tests that don't exercise this pass `undefined`).
|
|
609
|
+
*/
|
|
610
|
+
globalModulesDirs?: string[];
|
|
582
611
|
}
|
|
583
612
|
/**
|
|
584
613
|
* Compute the ordered, de-duplicated list of resolver anchors to try, each
|
|
@@ -600,6 +629,32 @@ interface AnchorEnv {
|
|
|
600
629
|
* Pure except for the injected `realpath` / `which` seams — unit-tested.
|
|
601
630
|
*/
|
|
602
631
|
declare function computeOpenClawSdkAnchors(deps: AnchorEnv): string[];
|
|
632
|
+
/**
|
|
633
|
+
* Decide what `startChatService()` should do given the current connection
|
|
634
|
+
* state. Pure so the branch logic — the load-bearing fix for the
|
|
635
|
+
* health-monitor no-op restart loop (Badi Junior, 2026-07-09) — is unit
|
|
636
|
+
* testable without standing up a real WebSocket or the OpenClaw SDK.
|
|
637
|
+
*
|
|
638
|
+
* OpenClaw calls the registered service's `start()` in two very different
|
|
639
|
+
* situations that a sticky activation flag could not tell apart:
|
|
640
|
+
* - a redundant duplicate (re-`activate()` per tool-discovery pass / reconnect)
|
|
641
|
+
* → must be a no-op ('skip'), and
|
|
642
|
+
* - a channel health-monitor REVIVE of a channel it decided is "stopped"
|
|
643
|
+
* (`health-monitor: [alfe:default] restarting (reason: stopped)`), sometimes
|
|
644
|
+
* with no preceding `stop()` → must actually reconnect.
|
|
645
|
+
*
|
|
646
|
+
* Keying on the live client fixes it:
|
|
647
|
+
* - a connect in flight, or an already-connected client → 'skip'
|
|
648
|
+
* - a client that exists but is not connected → 'recycle' it then reconnect
|
|
649
|
+
* - no client at all → 'connect'
|
|
650
|
+
*
|
|
651
|
+
* Exported for unit tests.
|
|
652
|
+
*/
|
|
653
|
+
declare function decideChatServiceStart(state: {
|
|
654
|
+
connectInFlight: boolean;
|
|
655
|
+
clientPresent: boolean;
|
|
656
|
+
clientConnected: boolean;
|
|
657
|
+
}): 'skip' | 'recycle' | 'connect';
|
|
603
658
|
declare function enqueueAgentTurn(task: () => Promise<void>, opts?: {
|
|
604
659
|
coalesceKey?: string;
|
|
605
660
|
onSuperseded?: () => void;
|
|
@@ -681,4 +736,4 @@ declare const plugin: {
|
|
|
681
736
|
deactivate(api: PluginApi): Promise<void>;
|
|
682
737
|
};
|
|
683
738
|
//#endregion
|
|
684
|
-
export {
|
|
739
|
+
export { AlfeResolvedAccount as C, SessionSummary as E, AlfePluginConfig as S, SessionData as T, resolveAbortTargetKeys as _, VOICE_REPLY_SYSTEM_PROMPT as a, createAlfeChannelPlugin as b, __setActiveRunForTest as c, buildOriginEnvelopeContext as d, buildToolActivity as f, plugin as g, joinAssistantText as h, RunEventGate as i, admitAgentEvent as l, decideChatServiceStart as m, AnchorEnv as n, __agentTurnQueueForTest as o, computeOpenClawSdkAnchors as p, CROSS_AGENT_TOOLS_SYSTEM_PROMPT as r, __resetAgentTurnQueueForTest as s, ActiveRunRef as t, buildA2ACompletePayload as u, rewriteAssistantText as v, ChatMessage as w, AlfeChannelConfig as x, stripLeakedToolSummary as y };
|
package/dist/plugin.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as __setActiveRunForTest, c as buildOriginEnvelopeContext, d as
|
|
2
|
-
export { CROSS_AGENT_TOOLS_SYSTEM_PROMPT, VOICE_REPLY_SYSTEM_PROMPT, __agentTurnQueueForTest, __resetAgentTurnQueueForTest, __setActiveRunForTest, admitAgentEvent, buildA2ACompletePayload, buildOriginEnvelopeContext, buildToolActivity, computeOpenClawSdkAnchors, plugin as default, joinAssistantText, resolveAbortTargetKeys, rewriteAssistantText, stripLeakedToolSummary };
|
|
1
|
+
import { a as __setActiveRunForTest, c as buildOriginEnvelopeContext, d as decideChatServiceStart, f as joinAssistantText, g as stripLeakedToolSummary, h as rewriteAssistantText, i as __resetAgentTurnQueueForTest, l as buildToolActivity, m as resolveAbortTargetKeys, n as VOICE_REPLY_SYSTEM_PROMPT, o as admitAgentEvent, p as plugin, r as __agentTurnQueueForTest, s as buildA2ACompletePayload, t as CROSS_AGENT_TOOLS_SYSTEM_PROMPT, u as computeOpenClawSdkAnchors } from "./plugin2.js";
|
|
2
|
+
export { CROSS_AGENT_TOOLS_SYSTEM_PROMPT, VOICE_REPLY_SYSTEM_PROMPT, __agentTurnQueueForTest, __resetAgentTurnQueueForTest, __setActiveRunForTest, admitAgentEvent, buildA2ACompletePayload, buildOriginEnvelopeContext, buildToolActivity, computeOpenClawSdkAnchors, decideChatServiceStart, plugin as default, joinAssistantText, resolveAbortTargetKeys, rewriteAssistantText, stripLeakedToolSummary };
|
package/dist/plugin2.cjs
CHANGED
|
@@ -3,6 +3,7 @@ let node_fs_promises = require("node:fs/promises");
|
|
|
3
3
|
let node_fs = require("node:fs");
|
|
4
4
|
let node_child_process = require("node:child_process");
|
|
5
5
|
let node_path = require("node:path");
|
|
6
|
+
let node_url = require("node:url");
|
|
6
7
|
let node_os = require("node:os");
|
|
7
8
|
let _alfe_ai_chat = require("@alfe.ai/chat");
|
|
8
9
|
let _alfe_ai_agent_api_client = require("@alfe.ai/agent-api-client");
|
|
@@ -210,7 +211,8 @@ async function uploadLocalFile(localPath, log, deps) {
|
|
|
210
211
|
url: presigned.downloadUrl,
|
|
211
212
|
filename,
|
|
212
213
|
mimeType,
|
|
213
|
-
size
|
|
214
|
+
size,
|
|
215
|
+
attachmentId: presigned.id
|
|
214
216
|
};
|
|
215
217
|
} catch (err) {
|
|
216
218
|
log.warn(`Outbound media upload failed for ${filename}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -874,7 +876,7 @@ async function createSession(sessionId, agentId, channel, tenantId, userId) {
|
|
|
874
876
|
return session;
|
|
875
877
|
}
|
|
876
878
|
const writeLocks = /* @__PURE__ */ new Map();
|
|
877
|
-
async function addMessage(sessionId, role, content, senderId, senderName, components) {
|
|
879
|
+
async function addMessage(sessionId, role, content, senderId, senderName, components, attachments) {
|
|
878
880
|
const next = (writeLocks.get(sessionId) ?? Promise.resolve()).then(async () => {
|
|
879
881
|
const session = await getSession(sessionId);
|
|
880
882
|
if (!session) return;
|
|
@@ -884,7 +886,8 @@ async function addMessage(sessionId, role, content, senderId, senderName, compon
|
|
|
884
886
|
timestamp: Date.now(),
|
|
885
887
|
...senderId ? { senderId } : {},
|
|
886
888
|
...senderName ? { senderName } : {},
|
|
887
|
-
...components?.length ? { components } : {}
|
|
889
|
+
...components?.length ? { components } : {},
|
|
890
|
+
...attachments?.length ? { attachments } : {}
|
|
888
891
|
});
|
|
889
892
|
await saveSession(session);
|
|
890
893
|
}).finally(() => {
|
|
@@ -2070,24 +2073,33 @@ let resolveRouteEnvelope = null;
|
|
|
2070
2073
|
* degrades `chat.abort` to a logged no-op (Stop still seals the UI) rather than
|
|
2071
2074
|
* a hard failure.
|
|
2072
2075
|
*/
|
|
2073
|
-
function loadOpenClawSdk(req, log, anchor) {
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2076
|
+
async function loadOpenClawSdk(req, log, anchor) {
|
|
2077
|
+
const loadSubpath = async (subpath) => {
|
|
2078
|
+
try {
|
|
2079
|
+
const mod = await import((0, node_url.pathToFileURL)(req.resolve(subpath)).href);
|
|
2080
|
+
return {
|
|
2081
|
+
...typeof mod.default === "object" ? mod.default : void 0,
|
|
2082
|
+
...mod
|
|
2083
|
+
};
|
|
2084
|
+
} catch {}
|
|
2085
|
+
try {
|
|
2086
|
+
return req(subpath);
|
|
2087
|
+
} catch {
|
|
2088
|
+
return;
|
|
2080
2089
|
}
|
|
2081
|
-
}
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2090
|
+
};
|
|
2091
|
+
let resolvedDispatch = false;
|
|
2092
|
+
const channelInbound = await loadSubpath("openclaw/plugin-sdk/channel-inbound");
|
|
2093
|
+
if (channelInbound?.dispatchInboundDirectDmWithRuntime) {
|
|
2094
|
+
dispatchInbound = channelInbound.dispatchInboundDirectDmWithRuntime;
|
|
2095
|
+
resolvedDispatch = true;
|
|
2096
|
+
}
|
|
2097
|
+
const harness = await loadSubpath("openclaw/plugin-sdk/agent-harness");
|
|
2098
|
+
const abortRun = harness?.abortAgentHarnessRun ?? harness?.abortEmbeddedAgentRun;
|
|
2099
|
+
if (abortRun) abortEmbeddedAgentRun = abortRun;
|
|
2100
|
+
if (harness?.resolveActiveEmbeddedRunSessionId) resolveActiveRunSessionId = harness.resolveActiveEmbeddedRunSessionId;
|
|
2101
|
+
const envelope = await loadSubpath("openclaw/plugin-sdk/inbound-envelope");
|
|
2102
|
+
if (envelope?.resolveInboundRouteEnvelopeBuilderWithRuntime) resolveRouteEnvelope = envelope.resolveInboundRouteEnvelopeBuilderWithRuntime;
|
|
2091
2103
|
if (resolvedDispatch) log.info(`Resolved OpenClaw SDK from ${anchor} (hard-abort=${abortEmbeddedAgentRun && resolveActiveRunSessionId ? "available" : "unavailable"})`);
|
|
2092
2104
|
return resolvedDispatch;
|
|
2093
2105
|
}
|
|
@@ -2158,6 +2170,7 @@ function computeOpenClawSdkAnchors(deps) {
|
|
|
2158
2170
|
try {
|
|
2159
2171
|
push(deps.which("openclaw"));
|
|
2160
2172
|
} catch {}
|
|
2173
|
+
for (const dir of deps.globalModulesDirs ?? []) push((0, node_path.join)(dir, "openclaw", "package.json"));
|
|
2161
2174
|
for (const base of [deps.execPath, safeRealpath(deps.realpath, deps.execPath)]) {
|
|
2162
2175
|
if (!base) continue;
|
|
2163
2176
|
try {
|
|
@@ -2176,6 +2189,27 @@ function safeRealpath(realpath, p) {
|
|
|
2176
2189
|
}
|
|
2177
2190
|
}
|
|
2178
2191
|
/**
|
|
2192
|
+
* Well-known global `node_modules` locations to try as SDK anchors, in priority
|
|
2193
|
+
* order. Environment-INDEPENDENT (no PATH / `which` / execPath), so they resolve
|
|
2194
|
+
* the SDK even when the in-process plugin context has a stripped PATH or a
|
|
2195
|
+
* sandboxed `child_process` (the homebrew-keg self-hosted macOS case where every
|
|
2196
|
+
* environment-derived anchor returns nothing). Non-existent candidates are
|
|
2197
|
+
* harmless — `createRequire` just fails and the next anchor runs. `NODE_PATH`,
|
|
2198
|
+
* when set, is an explicit module-dir list, so honour it too.
|
|
2199
|
+
*/
|
|
2200
|
+
function candidateGlobalModulesDirs() {
|
|
2201
|
+
const dirs = [
|
|
2202
|
+
"/opt/homebrew/lib/node_modules",
|
|
2203
|
+
"/usr/local/lib/node_modules",
|
|
2204
|
+
"/usr/lib/node_modules",
|
|
2205
|
+
"/usr/local/share/npm/lib/node_modules"
|
|
2206
|
+
];
|
|
2207
|
+
const home = process.env.HOME;
|
|
2208
|
+
if (home) dirs.push((0, node_path.join)(home, ".npm-global", "lib", "node_modules"));
|
|
2209
|
+
for (const d of (process.env.NODE_PATH ?? "").split(node_path.delimiter)) if (d) dirs.push(d);
|
|
2210
|
+
return dirs;
|
|
2211
|
+
}
|
|
2212
|
+
/**
|
|
2179
2213
|
* Resolve OpenClaw SDK functions from the running process.
|
|
2180
2214
|
*
|
|
2181
2215
|
* The openclaw package is NOT in the plugin's node_modules (it's a peer dep).
|
|
@@ -2184,23 +2218,123 @@ function safeRealpath(realpath, p) {
|
|
|
2184
2218
|
* relocated-node layouts), then to a `which openclaw` lookup, then fall back to
|
|
2185
2219
|
* deriving the global modules path from process.execPath.
|
|
2186
2220
|
*/
|
|
2187
|
-
function resolveOpenClawSdk(log) {
|
|
2221
|
+
async function resolveOpenClawSdk(log) {
|
|
2188
2222
|
const pathEnv = process.env.PATH;
|
|
2189
2223
|
const anchors = computeOpenClawSdkAnchors({
|
|
2190
2224
|
realpath: node_fs.realpathSync,
|
|
2191
2225
|
which: () => whichOpenClaw(pathEnv),
|
|
2192
2226
|
requireMainFilename: require$1.main?.filename,
|
|
2193
2227
|
argv1: process.argv[1],
|
|
2194
|
-
execPath: process.execPath
|
|
2228
|
+
execPath: process.execPath,
|
|
2229
|
+
globalModulesDirs: candidateGlobalModulesDirs()
|
|
2195
2230
|
});
|
|
2196
2231
|
for (const anchor of anchors) try {
|
|
2197
|
-
if (loadOpenClawSdk((0, node_module.createRequire)(anchor), log, anchor)) return;
|
|
2232
|
+
if (await loadOpenClawSdk((0, node_module.createRequire)(anchor), log, anchor)) return;
|
|
2198
2233
|
} catch {}
|
|
2199
|
-
log.warn(
|
|
2234
|
+
log.warn(`OpenClaw SDK not resolvable — chat dispatch will not work. argv1=${process.argv[1] ?? "<none>"} execPath=${process.execPath} PATH=${pathEnv ? "set" : "EMPTY"} anchorsTried=[${anchors.join(", ")}]`);
|
|
2200
2235
|
}
|
|
2201
2236
|
let pluginRuntime = null;
|
|
2202
2237
|
let chatClient = null;
|
|
2203
2238
|
let connectingPromise = null;
|
|
2239
|
+
/**
|
|
2240
|
+
* Decide what `startChatService()` should do given the current connection
|
|
2241
|
+
* state. Pure so the branch logic — the load-bearing fix for the
|
|
2242
|
+
* health-monitor no-op restart loop (Badi Junior, 2026-07-09) — is unit
|
|
2243
|
+
* testable without standing up a real WebSocket or the OpenClaw SDK.
|
|
2244
|
+
*
|
|
2245
|
+
* OpenClaw calls the registered service's `start()` in two very different
|
|
2246
|
+
* situations that a sticky activation flag could not tell apart:
|
|
2247
|
+
* - a redundant duplicate (re-`activate()` per tool-discovery pass / reconnect)
|
|
2248
|
+
* → must be a no-op ('skip'), and
|
|
2249
|
+
* - a channel health-monitor REVIVE of a channel it decided is "stopped"
|
|
2250
|
+
* (`health-monitor: [alfe:default] restarting (reason: stopped)`), sometimes
|
|
2251
|
+
* with no preceding `stop()` → must actually reconnect.
|
|
2252
|
+
*
|
|
2253
|
+
* Keying on the live client fixes it:
|
|
2254
|
+
* - a connect in flight, or an already-connected client → 'skip'
|
|
2255
|
+
* - a client that exists but is not connected → 'recycle' it then reconnect
|
|
2256
|
+
* - no client at all → 'connect'
|
|
2257
|
+
*
|
|
2258
|
+
* Exported for unit tests.
|
|
2259
|
+
*/
|
|
2260
|
+
function decideChatServiceStart(state) {
|
|
2261
|
+
if (state.connectInFlight) return "skip";
|
|
2262
|
+
if (state.clientPresent && state.clientConnected) return "skip";
|
|
2263
|
+
if (state.clientPresent) return "recycle";
|
|
2264
|
+
return "connect";
|
|
2265
|
+
}
|
|
2266
|
+
const STORED_ATTACHMENT_TYPES = new Set([
|
|
2267
|
+
"image",
|
|
2268
|
+
"video",
|
|
2269
|
+
"audio",
|
|
2270
|
+
"document",
|
|
2271
|
+
"file"
|
|
2272
|
+
]);
|
|
2273
|
+
/** Narrow a possibly-loose type string, inferring from the mime as a fallback. */
|
|
2274
|
+
function coerceStoredAttachmentType(type, mimeType) {
|
|
2275
|
+
if (type && STORED_ATTACHMENT_TYPES.has(type)) return type;
|
|
2276
|
+
const m = mimeType ?? "";
|
|
2277
|
+
if (m.startsWith("image/")) return "image";
|
|
2278
|
+
if (m.startsWith("video/")) return "video";
|
|
2279
|
+
if (m.startsWith("audio/")) return "audio";
|
|
2280
|
+
if (m === "application/pdf" || m.includes("document") || m.includes("spreadsheet") || m.includes("presentation")) return "document";
|
|
2281
|
+
return "file";
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Map inbound user attachments to the persisted reference shape (drops the
|
|
2285
|
+
* presigned url — it's re-signed on demand). Every composer upload carries a
|
|
2286
|
+
* chat-bucket `id`, so all are persisted.
|
|
2287
|
+
*/
|
|
2288
|
+
function userAttachmentsToStored(refs) {
|
|
2289
|
+
if (!refs?.length) return void 0;
|
|
2290
|
+
const out = [];
|
|
2291
|
+
for (const r of refs) {
|
|
2292
|
+
if (!r.id) continue;
|
|
2293
|
+
out.push({
|
|
2294
|
+
attachmentId: r.id,
|
|
2295
|
+
type: coerceStoredAttachmentType(r.type, r.mimeType),
|
|
2296
|
+
mimeType: r.mimeType,
|
|
2297
|
+
filename: r.filename ?? "file",
|
|
2298
|
+
...typeof r.size === "number" && r.size > 0 ? { size: r.size } : {}
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2301
|
+
return out.length ? out : void 0;
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* Map resolved assistant media to persisted references. Only media uploaded to
|
|
2305
|
+
* our bucket (carrying `attachmentId`) is replayable — remote-passthrough media
|
|
2306
|
+
* has no bindable record and stays live-URL-only.
|
|
2307
|
+
*/
|
|
2308
|
+
function assistantMediaToStored(media) {
|
|
2309
|
+
if (!media?.length) return void 0;
|
|
2310
|
+
const out = [];
|
|
2311
|
+
for (const m of media) {
|
|
2312
|
+
if (!m.attachmentId) continue;
|
|
2313
|
+
out.push({
|
|
2314
|
+
attachmentId: m.attachmentId,
|
|
2315
|
+
type: coerceStoredAttachmentType(void 0, m.mimeType),
|
|
2316
|
+
mimeType: m.mimeType ?? "application/octet-stream",
|
|
2317
|
+
filename: m.filename ?? "file",
|
|
2318
|
+
...typeof m.size === "number" && m.size > 0 ? { size: m.size } : {}
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
return out.length ? out : void 0;
|
|
2322
|
+
}
|
|
2323
|
+
/**
|
|
2324
|
+
* Project persisted attachment references onto the `sessions.get` wire shape.
|
|
2325
|
+
* `attachmentId` becomes the wire `id`; the URL is intentionally omitted — the
|
|
2326
|
+
* client resolves a short-lived signed URL on demand.
|
|
2327
|
+
*/
|
|
2328
|
+
function replayAttachments(atts) {
|
|
2329
|
+
if (!atts?.length) return void 0;
|
|
2330
|
+
return atts.map((a) => ({
|
|
2331
|
+
id: a.attachmentId,
|
|
2332
|
+
type: a.type,
|
|
2333
|
+
mimeType: a.mimeType,
|
|
2334
|
+
filename: a.filename,
|
|
2335
|
+
...typeof a.size === "number" ? { size: a.size } : {}
|
|
2336
|
+
}));
|
|
2337
|
+
}
|
|
2204
2338
|
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
2205
2339
|
const DOWNLOAD_TIMEOUT_MS = 3e4;
|
|
2206
2340
|
const MAX_REDIRECTS = 5;
|
|
@@ -2450,7 +2584,7 @@ async function handleAgentRequest(request, log) {
|
|
|
2450
2584
|
const cfg = runtime.config.loadConfig();
|
|
2451
2585
|
const sessionId = conversationId ?? legacySessionKey;
|
|
2452
2586
|
if (!await getSession(sessionId)) await createSession(sessionId, "", "alfe", tenantId, userId);
|
|
2453
|
-
await addMessage(sessionId, "user", message, userId ?? senderId, displayName ?? senderId);
|
|
2587
|
+
await addMessage(sessionId, "user", message, userId ?? senderId, displayName ?? senderId, void 0, userAttachmentsToStored(rawAttachments));
|
|
2454
2588
|
const runGate = {
|
|
2455
2589
|
runId: null,
|
|
2456
2590
|
sessionKey: null
|
|
@@ -2686,7 +2820,7 @@ async function handleAgentRequest(request, log) {
|
|
|
2686
2820
|
const note = formatMediaFailureNote(media.newFailures);
|
|
2687
2821
|
responseText = responseText ? `${responseText}\n\n${note}` : note;
|
|
2688
2822
|
}
|
|
2689
|
-
await addMessage(sessionId, "assistant", responseText);
|
|
2823
|
+
await addMessage(sessionId, "assistant", responseText, void 0, void 0, void 0, assistantMediaToStored(media.attachments));
|
|
2690
2824
|
if (isA2A) a2aResponseBuffer += (a2aResponseBuffer && responseText ? "\n\n" : "") + responseText;
|
|
2691
2825
|
chatClient?.notify("agent-message", {
|
|
2692
2826
|
conversationId: conversationId ?? legacySessionKey,
|
|
@@ -2799,7 +2933,8 @@ async function handleSessionsGet(request, log) {
|
|
|
2799
2933
|
role: m.role,
|
|
2800
2934
|
content: m.content,
|
|
2801
2935
|
timestamp: m.timestamp,
|
|
2802
|
-
...m.components?.length ? { components: m.components } : {}
|
|
2936
|
+
...m.components?.length ? { components: m.components } : {},
|
|
2937
|
+
...m.attachments?.length ? { attachments: replayAttachments(m.attachments) } : {}
|
|
2803
2938
|
})),
|
|
2804
2939
|
activity: await resolveHistoryActivity(session)
|
|
2805
2940
|
});
|
|
@@ -2843,16 +2978,31 @@ const plugin = {
|
|
|
2843
2978
|
log.info(`Registered ${String(a2aTools.length)} agent tools`);
|
|
2844
2979
|
}
|
|
2845
2980
|
const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
|
|
2846
|
-
const startChatService = () => {
|
|
2847
|
-
|
|
2848
|
-
|
|
2981
|
+
const startChatService = async () => {
|
|
2982
|
+
const action = decideChatServiceStart({
|
|
2983
|
+
connectInFlight: connectingPromise !== null,
|
|
2984
|
+
clientPresent: chatClient !== null,
|
|
2985
|
+
clientConnected: chatClient?.isConnected ?? false
|
|
2986
|
+
});
|
|
2987
|
+
if (action === "skip") {
|
|
2988
|
+
log.debug("Chat service already connected or connecting — skipping duplicate start");
|
|
2849
2989
|
return;
|
|
2850
2990
|
}
|
|
2991
|
+
if (action === "recycle") {
|
|
2992
|
+
log.info("Chat service client present but not connected — recycling before reconnect");
|
|
2993
|
+
try {
|
|
2994
|
+
chatClient?.stop();
|
|
2995
|
+
} catch (err) {
|
|
2996
|
+
log.debug(`Recycle of stale chat client failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2997
|
+
}
|
|
2998
|
+
chatClient = null;
|
|
2999
|
+
}
|
|
2851
3000
|
globalThis.__alfeChatPluginActivated = true;
|
|
2852
3001
|
log.info("Chat plugin registering...");
|
|
2853
|
-
resolveOpenClawSdk(log);
|
|
3002
|
+
await resolveOpenClawSdk(log);
|
|
2854
3003
|
pluginRuntime = api.runtime ?? null;
|
|
2855
|
-
|
|
3004
|
+
let thisConnect = null;
|
|
3005
|
+
thisConnect = Promise.resolve().then(() => {
|
|
2856
3006
|
try {
|
|
2857
3007
|
const { apiKey, chatWsUrl } = (0, _alfe_ai_chat.resolveAlfeChat)({
|
|
2858
3008
|
apiKey: pluginConfig.apiKey,
|
|
@@ -2900,7 +3050,10 @@ const plugin = {
|
|
|
2900
3050
|
} catch (err) {
|
|
2901
3051
|
log.error(`Failed to initialize chat service: ${err instanceof Error ? err.message : String(err)}`);
|
|
2902
3052
|
}
|
|
3053
|
+
}).finally(() => {
|
|
3054
|
+
if (connectingPromise === thisConnect) connectingPromise = null;
|
|
2903
3055
|
});
|
|
3056
|
+
connectingPromise = thisConnect;
|
|
2904
3057
|
};
|
|
2905
3058
|
const stopChatService = async () => {
|
|
2906
3059
|
globalThis.__alfeChatPluginActivated = false;
|
|
@@ -2952,7 +3105,8 @@ const plugin = {
|
|
|
2952
3105
|
role: m.role,
|
|
2953
3106
|
content: m.content,
|
|
2954
3107
|
timestamp: m.timestamp,
|
|
2955
|
-
...m.components?.length ? { components: m.components } : {}
|
|
3108
|
+
...m.components?.length ? { components: m.components } : {},
|
|
3109
|
+
...m.attachments?.length ? { attachments: replayAttachments(m.attachments) } : {}
|
|
2956
3110
|
})),
|
|
2957
3111
|
activity: await resolveHistoryActivity(session)
|
|
2958
3112
|
};
|
|
@@ -3079,6 +3233,12 @@ Object.defineProperty(exports, "createAlfeChannelPlugin", {
|
|
|
3079
3233
|
return createAlfeChannelPlugin;
|
|
3080
3234
|
}
|
|
3081
3235
|
});
|
|
3236
|
+
Object.defineProperty(exports, "decideChatServiceStart", {
|
|
3237
|
+
enumerable: true,
|
|
3238
|
+
get: function() {
|
|
3239
|
+
return decideChatServiceStart;
|
|
3240
|
+
}
|
|
3241
|
+
});
|
|
3082
3242
|
Object.defineProperty(exports, "joinAssistantText", {
|
|
3083
3243
|
enumerable: true,
|
|
3084
3244
|
get: function() {
|
package/dist/plugin2.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as
|
|
2
|
-
export { ActiveRunRef, AnchorEnv, CROSS_AGENT_TOOLS_SYSTEM_PROMPT, RunEventGate, VOICE_REPLY_SYSTEM_PROMPT, __agentTurnQueueForTest, __resetAgentTurnQueueForTest, __setActiveRunForTest, admitAgentEvent, buildA2ACompletePayload, buildOriginEnvelopeContext, buildToolActivity, computeOpenClawSdkAnchors, plugin as default, joinAssistantText, resolveAbortTargetKeys, rewriteAssistantText, stripLeakedToolSummary };
|
|
1
|
+
import { _ as resolveAbortTargetKeys, a as VOICE_REPLY_SYSTEM_PROMPT, c as __setActiveRunForTest, d as buildOriginEnvelopeContext, f as buildToolActivity, g as plugin, h as joinAssistantText, i as RunEventGate, l as admitAgentEvent, m as decideChatServiceStart, n as AnchorEnv, o as __agentTurnQueueForTest, p as computeOpenClawSdkAnchors, r as CROSS_AGENT_TOOLS_SYSTEM_PROMPT, s as __resetAgentTurnQueueForTest, t as ActiveRunRef, u as buildA2ACompletePayload, v as rewriteAssistantText, y as stripLeakedToolSummary } from "./plugin.cjs";
|
|
2
|
+
export { ActiveRunRef, AnchorEnv, CROSS_AGENT_TOOLS_SYSTEM_PROMPT, RunEventGate, VOICE_REPLY_SYSTEM_PROMPT, __agentTurnQueueForTest, __resetAgentTurnQueueForTest, __setActiveRunForTest, admitAgentEvent, buildA2ACompletePayload, buildOriginEnvelopeContext, buildToolActivity, computeOpenClawSdkAnchors, decideChatServiceStart, plugin as default, joinAssistantText, resolveAbortTargetKeys, rewriteAssistantText, stripLeakedToolSummary };
|
package/dist/plugin2.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as
|
|
2
|
-
export { ActiveRunRef, AnchorEnv, CROSS_AGENT_TOOLS_SYSTEM_PROMPT, RunEventGate, VOICE_REPLY_SYSTEM_PROMPT, __agentTurnQueueForTest, __resetAgentTurnQueueForTest, __setActiveRunForTest, admitAgentEvent, buildA2ACompletePayload, buildOriginEnvelopeContext, buildToolActivity, computeOpenClawSdkAnchors, plugin as default, joinAssistantText, resolveAbortTargetKeys, rewriteAssistantText, stripLeakedToolSummary };
|
|
1
|
+
import { _ as resolveAbortTargetKeys, a as VOICE_REPLY_SYSTEM_PROMPT, c as __setActiveRunForTest, d as buildOriginEnvelopeContext, f as buildToolActivity, g as plugin, h as joinAssistantText, i as RunEventGate, l as admitAgentEvent, m as decideChatServiceStart, n as AnchorEnv, o as __agentTurnQueueForTest, p as computeOpenClawSdkAnchors, r as CROSS_AGENT_TOOLS_SYSTEM_PROMPT, s as __resetAgentTurnQueueForTest, t as ActiveRunRef, u as buildA2ACompletePayload, v as rewriteAssistantText, y as stripLeakedToolSummary } from "./plugin.js";
|
|
2
|
+
export { ActiveRunRef, AnchorEnv, CROSS_AGENT_TOOLS_SYSTEM_PROMPT, RunEventGate, VOICE_REPLY_SYSTEM_PROMPT, __agentTurnQueueForTest, __resetAgentTurnQueueForTest, __setActiveRunForTest, admitAgentEvent, buildA2ACompletePayload, buildOriginEnvelopeContext, buildToolActivity, computeOpenClawSdkAnchors, decideChatServiceStart, plugin as default, joinAssistantText, resolveAbortTargetKeys, rewriteAssistantText, stripLeakedToolSummary };
|
package/dist/plugin2.js
CHANGED
|
@@ -3,6 +3,7 @@ import { mkdir, open, readFile, readdir, stat, unlink, writeFile } from "node:fs
|
|
|
3
3
|
import { existsSync, realpathSync } from "node:fs";
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import { basename, delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
6
7
|
import { homedir } from "node:os";
|
|
7
8
|
import { ChatServiceClient, resolveAlfeChat } from "@alfe.ai/chat";
|
|
8
9
|
import { AgentApiClient, installToolErrorCapture } from "@alfe.ai/agent-api-client";
|
|
@@ -210,7 +211,8 @@ async function uploadLocalFile(localPath, log, deps) {
|
|
|
210
211
|
url: presigned.downloadUrl,
|
|
211
212
|
filename,
|
|
212
213
|
mimeType,
|
|
213
|
-
size
|
|
214
|
+
size,
|
|
215
|
+
attachmentId: presigned.id
|
|
214
216
|
};
|
|
215
217
|
} catch (err) {
|
|
216
218
|
log.warn(`Outbound media upload failed for ${filename}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -874,7 +876,7 @@ async function createSession(sessionId, agentId, channel, tenantId, userId) {
|
|
|
874
876
|
return session;
|
|
875
877
|
}
|
|
876
878
|
const writeLocks = /* @__PURE__ */ new Map();
|
|
877
|
-
async function addMessage(sessionId, role, content, senderId, senderName, components) {
|
|
879
|
+
async function addMessage(sessionId, role, content, senderId, senderName, components, attachments) {
|
|
878
880
|
const next = (writeLocks.get(sessionId) ?? Promise.resolve()).then(async () => {
|
|
879
881
|
const session = await getSession(sessionId);
|
|
880
882
|
if (!session) return;
|
|
@@ -884,7 +886,8 @@ async function addMessage(sessionId, role, content, senderId, senderName, compon
|
|
|
884
886
|
timestamp: Date.now(),
|
|
885
887
|
...senderId ? { senderId } : {},
|
|
886
888
|
...senderName ? { senderName } : {},
|
|
887
|
-
...components?.length ? { components } : {}
|
|
889
|
+
...components?.length ? { components } : {},
|
|
890
|
+
...attachments?.length ? { attachments } : {}
|
|
888
891
|
});
|
|
889
892
|
await saveSession(session);
|
|
890
893
|
}).finally(() => {
|
|
@@ -2070,24 +2073,33 @@ let resolveRouteEnvelope = null;
|
|
|
2070
2073
|
* degrades `chat.abort` to a logged no-op (Stop still seals the UI) rather than
|
|
2071
2074
|
* a hard failure.
|
|
2072
2075
|
*/
|
|
2073
|
-
function loadOpenClawSdk(req, log, anchor) {
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2076
|
+
async function loadOpenClawSdk(req, log, anchor) {
|
|
2077
|
+
const loadSubpath = async (subpath) => {
|
|
2078
|
+
try {
|
|
2079
|
+
const mod = await import(pathToFileURL(req.resolve(subpath)).href);
|
|
2080
|
+
return {
|
|
2081
|
+
...typeof mod.default === "object" ? mod.default : void 0,
|
|
2082
|
+
...mod
|
|
2083
|
+
};
|
|
2084
|
+
} catch {}
|
|
2085
|
+
try {
|
|
2086
|
+
return req(subpath);
|
|
2087
|
+
} catch {
|
|
2088
|
+
return;
|
|
2080
2089
|
}
|
|
2081
|
-
}
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2090
|
+
};
|
|
2091
|
+
let resolvedDispatch = false;
|
|
2092
|
+
const channelInbound = await loadSubpath("openclaw/plugin-sdk/channel-inbound");
|
|
2093
|
+
if (channelInbound?.dispatchInboundDirectDmWithRuntime) {
|
|
2094
|
+
dispatchInbound = channelInbound.dispatchInboundDirectDmWithRuntime;
|
|
2095
|
+
resolvedDispatch = true;
|
|
2096
|
+
}
|
|
2097
|
+
const harness = await loadSubpath("openclaw/plugin-sdk/agent-harness");
|
|
2098
|
+
const abortRun = harness?.abortAgentHarnessRun ?? harness?.abortEmbeddedAgentRun;
|
|
2099
|
+
if (abortRun) abortEmbeddedAgentRun = abortRun;
|
|
2100
|
+
if (harness?.resolveActiveEmbeddedRunSessionId) resolveActiveRunSessionId = harness.resolveActiveEmbeddedRunSessionId;
|
|
2101
|
+
const envelope = await loadSubpath("openclaw/plugin-sdk/inbound-envelope");
|
|
2102
|
+
if (envelope?.resolveInboundRouteEnvelopeBuilderWithRuntime) resolveRouteEnvelope = envelope.resolveInboundRouteEnvelopeBuilderWithRuntime;
|
|
2091
2103
|
if (resolvedDispatch) log.info(`Resolved OpenClaw SDK from ${anchor} (hard-abort=${abortEmbeddedAgentRun && resolveActiveRunSessionId ? "available" : "unavailable"})`);
|
|
2092
2104
|
return resolvedDispatch;
|
|
2093
2105
|
}
|
|
@@ -2158,6 +2170,7 @@ function computeOpenClawSdkAnchors(deps) {
|
|
|
2158
2170
|
try {
|
|
2159
2171
|
push(deps.which("openclaw"));
|
|
2160
2172
|
} catch {}
|
|
2173
|
+
for (const dir of deps.globalModulesDirs ?? []) push(join(dir, "openclaw", "package.json"));
|
|
2161
2174
|
for (const base of [deps.execPath, safeRealpath(deps.realpath, deps.execPath)]) {
|
|
2162
2175
|
if (!base) continue;
|
|
2163
2176
|
try {
|
|
@@ -2176,6 +2189,27 @@ function safeRealpath(realpath, p) {
|
|
|
2176
2189
|
}
|
|
2177
2190
|
}
|
|
2178
2191
|
/**
|
|
2192
|
+
* Well-known global `node_modules` locations to try as SDK anchors, in priority
|
|
2193
|
+
* order. Environment-INDEPENDENT (no PATH / `which` / execPath), so they resolve
|
|
2194
|
+
* the SDK even when the in-process plugin context has a stripped PATH or a
|
|
2195
|
+
* sandboxed `child_process` (the homebrew-keg self-hosted macOS case where every
|
|
2196
|
+
* environment-derived anchor returns nothing). Non-existent candidates are
|
|
2197
|
+
* harmless — `createRequire` just fails and the next anchor runs. `NODE_PATH`,
|
|
2198
|
+
* when set, is an explicit module-dir list, so honour it too.
|
|
2199
|
+
*/
|
|
2200
|
+
function candidateGlobalModulesDirs() {
|
|
2201
|
+
const dirs = [
|
|
2202
|
+
"/opt/homebrew/lib/node_modules",
|
|
2203
|
+
"/usr/local/lib/node_modules",
|
|
2204
|
+
"/usr/lib/node_modules",
|
|
2205
|
+
"/usr/local/share/npm/lib/node_modules"
|
|
2206
|
+
];
|
|
2207
|
+
const home = process.env.HOME;
|
|
2208
|
+
if (home) dirs.push(join(home, ".npm-global", "lib", "node_modules"));
|
|
2209
|
+
for (const d of (process.env.NODE_PATH ?? "").split(delimiter)) if (d) dirs.push(d);
|
|
2210
|
+
return dirs;
|
|
2211
|
+
}
|
|
2212
|
+
/**
|
|
2179
2213
|
* Resolve OpenClaw SDK functions from the running process.
|
|
2180
2214
|
*
|
|
2181
2215
|
* The openclaw package is NOT in the plugin's node_modules (it's a peer dep).
|
|
@@ -2184,23 +2218,123 @@ function safeRealpath(realpath, p) {
|
|
|
2184
2218
|
* relocated-node layouts), then to a `which openclaw` lookup, then fall back to
|
|
2185
2219
|
* deriving the global modules path from process.execPath.
|
|
2186
2220
|
*/
|
|
2187
|
-
function resolveOpenClawSdk(log) {
|
|
2221
|
+
async function resolveOpenClawSdk(log) {
|
|
2188
2222
|
const pathEnv = process.env.PATH;
|
|
2189
2223
|
const anchors = computeOpenClawSdkAnchors({
|
|
2190
2224
|
realpath: realpathSync,
|
|
2191
2225
|
which: () => whichOpenClaw(pathEnv),
|
|
2192
2226
|
requireMainFilename: require.main?.filename,
|
|
2193
2227
|
argv1: process.argv[1],
|
|
2194
|
-
execPath: process.execPath
|
|
2228
|
+
execPath: process.execPath,
|
|
2229
|
+
globalModulesDirs: candidateGlobalModulesDirs()
|
|
2195
2230
|
});
|
|
2196
2231
|
for (const anchor of anchors) try {
|
|
2197
|
-
if (loadOpenClawSdk(createRequire(anchor), log, anchor)) return;
|
|
2232
|
+
if (await loadOpenClawSdk(createRequire(anchor), log, anchor)) return;
|
|
2198
2233
|
} catch {}
|
|
2199
|
-
log.warn(
|
|
2234
|
+
log.warn(`OpenClaw SDK not resolvable — chat dispatch will not work. argv1=${process.argv[1] ?? "<none>"} execPath=${process.execPath} PATH=${pathEnv ? "set" : "EMPTY"} anchorsTried=[${anchors.join(", ")}]`);
|
|
2200
2235
|
}
|
|
2201
2236
|
let pluginRuntime = null;
|
|
2202
2237
|
let chatClient = null;
|
|
2203
2238
|
let connectingPromise = null;
|
|
2239
|
+
/**
|
|
2240
|
+
* Decide what `startChatService()` should do given the current connection
|
|
2241
|
+
* state. Pure so the branch logic — the load-bearing fix for the
|
|
2242
|
+
* health-monitor no-op restart loop (Badi Junior, 2026-07-09) — is unit
|
|
2243
|
+
* testable without standing up a real WebSocket or the OpenClaw SDK.
|
|
2244
|
+
*
|
|
2245
|
+
* OpenClaw calls the registered service's `start()` in two very different
|
|
2246
|
+
* situations that a sticky activation flag could not tell apart:
|
|
2247
|
+
* - a redundant duplicate (re-`activate()` per tool-discovery pass / reconnect)
|
|
2248
|
+
* → must be a no-op ('skip'), and
|
|
2249
|
+
* - a channel health-monitor REVIVE of a channel it decided is "stopped"
|
|
2250
|
+
* (`health-monitor: [alfe:default] restarting (reason: stopped)`), sometimes
|
|
2251
|
+
* with no preceding `stop()` → must actually reconnect.
|
|
2252
|
+
*
|
|
2253
|
+
* Keying on the live client fixes it:
|
|
2254
|
+
* - a connect in flight, or an already-connected client → 'skip'
|
|
2255
|
+
* - a client that exists but is not connected → 'recycle' it then reconnect
|
|
2256
|
+
* - no client at all → 'connect'
|
|
2257
|
+
*
|
|
2258
|
+
* Exported for unit tests.
|
|
2259
|
+
*/
|
|
2260
|
+
function decideChatServiceStart(state) {
|
|
2261
|
+
if (state.connectInFlight) return "skip";
|
|
2262
|
+
if (state.clientPresent && state.clientConnected) return "skip";
|
|
2263
|
+
if (state.clientPresent) return "recycle";
|
|
2264
|
+
return "connect";
|
|
2265
|
+
}
|
|
2266
|
+
const STORED_ATTACHMENT_TYPES = new Set([
|
|
2267
|
+
"image",
|
|
2268
|
+
"video",
|
|
2269
|
+
"audio",
|
|
2270
|
+
"document",
|
|
2271
|
+
"file"
|
|
2272
|
+
]);
|
|
2273
|
+
/** Narrow a possibly-loose type string, inferring from the mime as a fallback. */
|
|
2274
|
+
function coerceStoredAttachmentType(type, mimeType) {
|
|
2275
|
+
if (type && STORED_ATTACHMENT_TYPES.has(type)) return type;
|
|
2276
|
+
const m = mimeType ?? "";
|
|
2277
|
+
if (m.startsWith("image/")) return "image";
|
|
2278
|
+
if (m.startsWith("video/")) return "video";
|
|
2279
|
+
if (m.startsWith("audio/")) return "audio";
|
|
2280
|
+
if (m === "application/pdf" || m.includes("document") || m.includes("spreadsheet") || m.includes("presentation")) return "document";
|
|
2281
|
+
return "file";
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Map inbound user attachments to the persisted reference shape (drops the
|
|
2285
|
+
* presigned url — it's re-signed on demand). Every composer upload carries a
|
|
2286
|
+
* chat-bucket `id`, so all are persisted.
|
|
2287
|
+
*/
|
|
2288
|
+
function userAttachmentsToStored(refs) {
|
|
2289
|
+
if (!refs?.length) return void 0;
|
|
2290
|
+
const out = [];
|
|
2291
|
+
for (const r of refs) {
|
|
2292
|
+
if (!r.id) continue;
|
|
2293
|
+
out.push({
|
|
2294
|
+
attachmentId: r.id,
|
|
2295
|
+
type: coerceStoredAttachmentType(r.type, r.mimeType),
|
|
2296
|
+
mimeType: r.mimeType,
|
|
2297
|
+
filename: r.filename ?? "file",
|
|
2298
|
+
...typeof r.size === "number" && r.size > 0 ? { size: r.size } : {}
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2301
|
+
return out.length ? out : void 0;
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* Map resolved assistant media to persisted references. Only media uploaded to
|
|
2305
|
+
* our bucket (carrying `attachmentId`) is replayable — remote-passthrough media
|
|
2306
|
+
* has no bindable record and stays live-URL-only.
|
|
2307
|
+
*/
|
|
2308
|
+
function assistantMediaToStored(media) {
|
|
2309
|
+
if (!media?.length) return void 0;
|
|
2310
|
+
const out = [];
|
|
2311
|
+
for (const m of media) {
|
|
2312
|
+
if (!m.attachmentId) continue;
|
|
2313
|
+
out.push({
|
|
2314
|
+
attachmentId: m.attachmentId,
|
|
2315
|
+
type: coerceStoredAttachmentType(void 0, m.mimeType),
|
|
2316
|
+
mimeType: m.mimeType ?? "application/octet-stream",
|
|
2317
|
+
filename: m.filename ?? "file",
|
|
2318
|
+
...typeof m.size === "number" && m.size > 0 ? { size: m.size } : {}
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
return out.length ? out : void 0;
|
|
2322
|
+
}
|
|
2323
|
+
/**
|
|
2324
|
+
* Project persisted attachment references onto the `sessions.get` wire shape.
|
|
2325
|
+
* `attachmentId` becomes the wire `id`; the URL is intentionally omitted — the
|
|
2326
|
+
* client resolves a short-lived signed URL on demand.
|
|
2327
|
+
*/
|
|
2328
|
+
function replayAttachments(atts) {
|
|
2329
|
+
if (!atts?.length) return void 0;
|
|
2330
|
+
return atts.map((a) => ({
|
|
2331
|
+
id: a.attachmentId,
|
|
2332
|
+
type: a.type,
|
|
2333
|
+
mimeType: a.mimeType,
|
|
2334
|
+
filename: a.filename,
|
|
2335
|
+
...typeof a.size === "number" ? { size: a.size } : {}
|
|
2336
|
+
}));
|
|
2337
|
+
}
|
|
2204
2338
|
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
2205
2339
|
const DOWNLOAD_TIMEOUT_MS = 3e4;
|
|
2206
2340
|
const MAX_REDIRECTS = 5;
|
|
@@ -2450,7 +2584,7 @@ async function handleAgentRequest(request, log) {
|
|
|
2450
2584
|
const cfg = runtime.config.loadConfig();
|
|
2451
2585
|
const sessionId = conversationId ?? legacySessionKey;
|
|
2452
2586
|
if (!await getSession(sessionId)) await createSession(sessionId, "", "alfe", tenantId, userId);
|
|
2453
|
-
await addMessage(sessionId, "user", message, userId ?? senderId, displayName ?? senderId);
|
|
2587
|
+
await addMessage(sessionId, "user", message, userId ?? senderId, displayName ?? senderId, void 0, userAttachmentsToStored(rawAttachments));
|
|
2454
2588
|
const runGate = {
|
|
2455
2589
|
runId: null,
|
|
2456
2590
|
sessionKey: null
|
|
@@ -2686,7 +2820,7 @@ async function handleAgentRequest(request, log) {
|
|
|
2686
2820
|
const note = formatMediaFailureNote(media.newFailures);
|
|
2687
2821
|
responseText = responseText ? `${responseText}\n\n${note}` : note;
|
|
2688
2822
|
}
|
|
2689
|
-
await addMessage(sessionId, "assistant", responseText);
|
|
2823
|
+
await addMessage(sessionId, "assistant", responseText, void 0, void 0, void 0, assistantMediaToStored(media.attachments));
|
|
2690
2824
|
if (isA2A) a2aResponseBuffer += (a2aResponseBuffer && responseText ? "\n\n" : "") + responseText;
|
|
2691
2825
|
chatClient?.notify("agent-message", {
|
|
2692
2826
|
conversationId: conversationId ?? legacySessionKey,
|
|
@@ -2799,7 +2933,8 @@ async function handleSessionsGet(request, log) {
|
|
|
2799
2933
|
role: m.role,
|
|
2800
2934
|
content: m.content,
|
|
2801
2935
|
timestamp: m.timestamp,
|
|
2802
|
-
...m.components?.length ? { components: m.components } : {}
|
|
2936
|
+
...m.components?.length ? { components: m.components } : {},
|
|
2937
|
+
...m.attachments?.length ? { attachments: replayAttachments(m.attachments) } : {}
|
|
2803
2938
|
})),
|
|
2804
2939
|
activity: await resolveHistoryActivity(session)
|
|
2805
2940
|
});
|
|
@@ -2843,16 +2978,31 @@ const plugin = {
|
|
|
2843
2978
|
log.info(`Registered ${String(a2aTools.length)} agent tools`);
|
|
2844
2979
|
}
|
|
2845
2980
|
const pluginConfig = (((api.config ?? {}).plugins?.entries)?.["@alfe.ai/openclaw-chat"] ?? {}).config ?? {};
|
|
2846
|
-
const startChatService = () => {
|
|
2847
|
-
|
|
2848
|
-
|
|
2981
|
+
const startChatService = async () => {
|
|
2982
|
+
const action = decideChatServiceStart({
|
|
2983
|
+
connectInFlight: connectingPromise !== null,
|
|
2984
|
+
clientPresent: chatClient !== null,
|
|
2985
|
+
clientConnected: chatClient?.isConnected ?? false
|
|
2986
|
+
});
|
|
2987
|
+
if (action === "skip") {
|
|
2988
|
+
log.debug("Chat service already connected or connecting — skipping duplicate start");
|
|
2849
2989
|
return;
|
|
2850
2990
|
}
|
|
2991
|
+
if (action === "recycle") {
|
|
2992
|
+
log.info("Chat service client present but not connected — recycling before reconnect");
|
|
2993
|
+
try {
|
|
2994
|
+
chatClient?.stop();
|
|
2995
|
+
} catch (err) {
|
|
2996
|
+
log.debug(`Recycle of stale chat client failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2997
|
+
}
|
|
2998
|
+
chatClient = null;
|
|
2999
|
+
}
|
|
2851
3000
|
globalThis.__alfeChatPluginActivated = true;
|
|
2852
3001
|
log.info("Chat plugin registering...");
|
|
2853
|
-
resolveOpenClawSdk(log);
|
|
3002
|
+
await resolveOpenClawSdk(log);
|
|
2854
3003
|
pluginRuntime = api.runtime ?? null;
|
|
2855
|
-
|
|
3004
|
+
let thisConnect = null;
|
|
3005
|
+
thisConnect = Promise.resolve().then(() => {
|
|
2856
3006
|
try {
|
|
2857
3007
|
const { apiKey, chatWsUrl } = resolveAlfeChat({
|
|
2858
3008
|
apiKey: pluginConfig.apiKey,
|
|
@@ -2900,7 +3050,10 @@ const plugin = {
|
|
|
2900
3050
|
} catch (err) {
|
|
2901
3051
|
log.error(`Failed to initialize chat service: ${err instanceof Error ? err.message : String(err)}`);
|
|
2902
3052
|
}
|
|
3053
|
+
}).finally(() => {
|
|
3054
|
+
if (connectingPromise === thisConnect) connectingPromise = null;
|
|
2903
3055
|
});
|
|
3056
|
+
connectingPromise = thisConnect;
|
|
2904
3057
|
};
|
|
2905
3058
|
const stopChatService = async () => {
|
|
2906
3059
|
globalThis.__alfeChatPluginActivated = false;
|
|
@@ -2952,7 +3105,8 @@ const plugin = {
|
|
|
2952
3105
|
role: m.role,
|
|
2953
3106
|
content: m.content,
|
|
2954
3107
|
timestamp: m.timestamp,
|
|
2955
|
-
...m.components?.length ? { components: m.components } : {}
|
|
3108
|
+
...m.components?.length ? { components: m.components } : {},
|
|
3109
|
+
...m.attachments?.length ? { attachments: replayAttachments(m.attachments) } : {}
|
|
2956
3110
|
})),
|
|
2957
3111
|
activity: await resolveHistoryActivity(session)
|
|
2958
3112
|
};
|
|
@@ -3013,4 +3167,4 @@ const plugin = {
|
|
|
3013
3167
|
}
|
|
3014
3168
|
};
|
|
3015
3169
|
//#endregion
|
|
3016
|
-
export { __setActiveRunForTest as a, buildOriginEnvelopeContext as c,
|
|
3170
|
+
export { createAlfeChannelPlugin as _, __setActiveRunForTest as a, buildOriginEnvelopeContext as c, decideChatServiceStart as d, joinAssistantText as f, stripLeakedToolSummary as g, rewriteAssistantText as h, __resetAgentTurnQueueForTest as i, buildToolActivity as l, resolveAbortTargetKeys as m, VOICE_REPLY_SYSTEM_PROMPT as n, admitAgentEvent as o, plugin as p, __agentTurnQueueForTest as r, buildA2ACompletePayload as s, CROSS_AGENT_TOOLS_SYSTEM_PROMPT as t, computeOpenClawSdkAnchors as u };
|
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.6",
|
|
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.
|
|
30
|
+
"@alfe.ai/agent-api-client": "^0.11.0",
|
|
31
31
|
"@alfe.ai/chat": "^0.0.16",
|
|
32
32
|
"@alfe.ai/config": "^0.3.0"
|
|
33
33
|
},
|