@newbase-clawchat/openclaw-clawchat 2026.5.12-2 → 2026.5.12-21
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/README.md +39 -17
- package/dist/index.js +3 -1
- package/dist/src/api-client.js +71 -12
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/channel.js +5 -5
- package/dist/src/channel.setup.js +4 -17
- package/dist/src/clawchat-memory.js +290 -0
- package/dist/src/clawchat-metadata.js +235 -0
- package/dist/src/client.js +31 -93
- package/dist/src/commands.js +3 -3
- package/dist/src/config.js +58 -3
- package/dist/src/group-message-coalescer.js +107 -0
- package/dist/src/inbound.js +24 -28
- package/dist/src/login.runtime.js +82 -19
- package/dist/src/media-runtime.js +2 -3
- package/dist/src/message-mapper.js +1 -1
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +281 -56
- package/dist/src/plugin-prompts.js +76 -0
- package/dist/src/profile-prompt.js +150 -0
- package/dist/src/profile-sync.js +169 -0
- package/dist/src/prompt-injection.js +25 -0
- package/dist/src/protocol-types.js +63 -0
- package/dist/src/protocol-types.typecheck.js +1 -0
- package/dist/src/protocol.js +2 -2
- package/dist/src/reply-dispatcher.js +143 -40
- package/dist/src/runtime.js +813 -109
- package/dist/src/storage.js +636 -0
- package/dist/src/tools-schema.js +70 -10
- package/dist/src/tools.js +600 -112
- package/dist/src/ws-alignment.js +8 -0
- package/dist/src/ws-client.js +588 -0
- package/index.ts +6 -1
- package/openclaw.plugin.json +44 -4
- package/package.json +4 -3
- package/prompts/platform.md +7 -0
- package/skills/clawchat/SKILL.md +90 -0
- package/src/api-client.test.ts +360 -15
- package/src/api-client.ts +127 -25
- package/src/api-types.test-d.ts +12 -0
- package/src/api-types.ts +71 -4
- package/src/buffered-stream.test.ts +1 -1
- package/src/buffered-stream.ts +1 -1
- package/src/channel.outbound.test.ts +270 -60
- package/src/channel.setup.ts +9 -18
- package/src/channel.test.ts +33 -25
- package/src/channel.ts +5 -7
- package/src/clawchat-memory.test.ts +372 -0
- package/src/clawchat-memory.ts +363 -0
- package/src/clawchat-metadata.test.ts +350 -0
- package/src/clawchat-metadata.ts +352 -0
- package/src/client.test.ts +57 -48
- package/src/client.ts +37 -129
- package/src/commands.test.ts +2 -2
- package/src/commands.ts +3 -3
- package/src/config.test.ts +169 -4
- package/src/config.ts +86 -6
- package/src/group-message-coalescer.test.ts +223 -0
- package/src/group-message-coalescer.ts +154 -0
- package/src/inbound.test.ts +106 -19
- package/src/inbound.ts +31 -35
- package/src/login.runtime.test.ts +294 -11
- package/src/login.runtime.ts +90 -21
- package/src/manifest.test.ts +86 -14
- package/src/media-runtime.test.ts +31 -2
- package/src/media-runtime.ts +7 -10
- package/src/message-mapper.test.ts +2 -2
- package/src/message-mapper.ts +2 -2
- package/src/mock-transport.test.ts +35 -0
- package/src/mock-transport.ts +38 -0
- package/src/outbound.test.ts +811 -95
- package/src/outbound.ts +332 -65
- package/src/plugin-entry.test.ts +3 -1
- package/src/plugin-prompts.test.ts +78 -0
- package/src/plugin-prompts.ts +92 -0
- package/src/profile-prompt.test.ts +435 -0
- package/src/profile-prompt.ts +208 -0
- package/src/profile-sync.test.ts +611 -0
- package/src/profile-sync.ts +268 -0
- package/src/prompt-injection.test.ts +39 -0
- package/src/prompt-injection.ts +45 -0
- package/src/protocol-types.test.ts +69 -0
- package/src/protocol-types.ts +296 -0
- package/src/protocol-types.typecheck.ts +89 -0
- package/src/protocol.ts +2 -2
- package/src/reply-dispatcher.test.ts +720 -135
- package/src/reply-dispatcher.ts +174 -42
- package/src/runtime.test.ts +3884 -337
- package/src/runtime.ts +956 -128
- package/src/storage.test.ts +692 -0
- package/src/storage.ts +989 -0
- package/src/streaming.test.ts +1 -1
- package/src/streaming.ts +1 -1
- package/src/tools-schema.ts +115 -13
- package/src/tools.test.ts +501 -10
- package/src/tools.ts +739 -133
- package/src/ws-alignment.ts +9 -0
- package/src/ws-client.test.ts +1218 -0
- package/src/ws-client.ts +662 -0
package/src/runtime.ts
CHANGED
|
@@ -4,17 +4,25 @@ import {
|
|
|
4
4
|
ProtocolError,
|
|
5
5
|
StateError,
|
|
6
6
|
TransportError,
|
|
7
|
-
|
|
7
|
+
EVENT,
|
|
8
8
|
type Envelope,
|
|
9
9
|
type Transport,
|
|
10
|
-
} from "
|
|
10
|
+
} from "./protocol-types.ts";
|
|
11
11
|
import type { ChannelAccountSnapshot } from "openclaw/plugin-sdk/channel-contract";
|
|
12
12
|
import { waitUntilAbort } from "openclaw/plugin-sdk/channel-lifecycle";
|
|
13
|
+
import { hasControlCommand } from "openclaw/plugin-sdk/command-detection";
|
|
13
14
|
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/core";
|
|
14
15
|
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
15
16
|
import { createOpenclawClawlingClient } from "./client.ts";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
17
|
+
import { createOpenclawClawlingApiClient } from "./api-client.ts";
|
|
18
|
+
import { ClawlingApiError, type ConversationDetails } from "./api-types.ts";
|
|
19
|
+
import {
|
|
20
|
+
CHANNEL_ID,
|
|
21
|
+
effectiveGroupCommandMode,
|
|
22
|
+
type ResolvedOpenclawClawlingAccount,
|
|
23
|
+
} from "./config.ts";
|
|
24
|
+
import type { ClawlingChatClient } from "./ws-client.ts";
|
|
25
|
+
import { dispatchOpenclawClawlingInbound, type IngestTurnParams } from "./inbound.ts";
|
|
18
26
|
import { fetchInboundMedia } from "./media-runtime.ts";
|
|
19
27
|
import { createOpenclawClawlingReplyDispatcher } from "./reply-dispatcher.ts";
|
|
20
28
|
import { sendStreamingText } from "./streaming.ts";
|
|
@@ -25,8 +33,47 @@ import {
|
|
|
25
33
|
} from "./outbound.ts";
|
|
26
34
|
import { formatWsLog } from "./ws-log.ts";
|
|
27
35
|
import { createProtocolControlHandler, createReconnectTracker } from "./ws-alignment.ts";
|
|
36
|
+
import {
|
|
37
|
+
clawChatDbPathForStateDir,
|
|
38
|
+
getClawChatStore,
|
|
39
|
+
type ClawChatStore,
|
|
40
|
+
} from "./storage.ts";
|
|
41
|
+
import { getClawChatGroupPrompt, getClawChatUserPrompt } from "./plugin-prompts.ts";
|
|
42
|
+
import {
|
|
43
|
+
loadClawChatPromptMetadata,
|
|
44
|
+
renderClawChatProfilePrompt,
|
|
45
|
+
resolveSenderRelation,
|
|
46
|
+
} from "./profile-prompt.ts";
|
|
47
|
+
import { refreshGroupProfile, syncFirstSeenClawChatProfiles } from "./profile-sync.ts";
|
|
48
|
+
import { pullGroupMetadata, pullOwnerMetadata } from "./clawchat-metadata.ts";
|
|
49
|
+
import {
|
|
50
|
+
clearClawChatPromptInjectionForSession,
|
|
51
|
+
stageClawChatPromptInjection,
|
|
52
|
+
} from "./prompt-injection.ts";
|
|
53
|
+
import { createGroupMessageCoalescer } from "./group-message-coalescer.ts";
|
|
28
54
|
|
|
29
55
|
type Log = { info?: (m: string) => void; error?: (m: string) => void };
|
|
56
|
+
type RuntimeConnectionStore = Pick<
|
|
57
|
+
ClawChatStore,
|
|
58
|
+
"startConnection" | "markConnectSent" | "markConnectionReady" | "finishConnection"
|
|
59
|
+
> &
|
|
60
|
+
Partial<
|
|
61
|
+
Pick<
|
|
62
|
+
ClawChatStore,
|
|
63
|
+
| "insertMessage"
|
|
64
|
+
| "claimMessageOnce"
|
|
65
|
+
| "updateMessageByIdentity"
|
|
66
|
+
| "claimPendingActivationBootstrap"
|
|
67
|
+
| "releaseActivationBootstrapClaim"
|
|
68
|
+
| "markActivationBootstrapSent"
|
|
69
|
+
| "upsertConversationSummary"
|
|
70
|
+
| "upsertConversationDetails"
|
|
71
|
+
| "deleteConversationCache"
|
|
72
|
+
| "listCachedConversationIds"
|
|
73
|
+
| "getActivationConversation"
|
|
74
|
+
| "getCachedConversation"
|
|
75
|
+
>
|
|
76
|
+
>;
|
|
30
77
|
|
|
31
78
|
const { setRuntime: setOpenclawClawlingRuntime, getRuntime: getOpenclawClawlingRuntime } =
|
|
32
79
|
createPluginRuntimeStore<PluginRuntime>("openclaw-clawchat runtime not initialized");
|
|
@@ -34,11 +81,67 @@ const { setRuntime: setOpenclawClawlingRuntime, getRuntime: getOpenclawClawlingR
|
|
|
34
81
|
export { setOpenclawClawlingRuntime, getOpenclawClawlingRuntime };
|
|
35
82
|
|
|
36
83
|
const activeClients = new Map<string, ClawlingChatClient>();
|
|
84
|
+
const CLAWCHAT_PLUGIN_SLASH_COMMANDS = new Set(["clawchat-activate"]);
|
|
85
|
+
const CLAWCHAT_MEMORY_ROOT_UNAVAILABLE =
|
|
86
|
+
"ClawChat memory root unavailable: OpenClaw workspaceDir could not be resolved";
|
|
87
|
+
const OPENCLAW_CONFIRM_SLASH_COMMANDS = new Set([
|
|
88
|
+
"approve",
|
|
89
|
+
"deny",
|
|
90
|
+
"always",
|
|
91
|
+
"cancel",
|
|
92
|
+
"yes",
|
|
93
|
+
"no",
|
|
94
|
+
"ok",
|
|
95
|
+
"confirm",
|
|
96
|
+
"remember",
|
|
97
|
+
"nevermind",
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
function parseSlashCommandName(rawBody: string): string | null {
|
|
101
|
+
const stripped = rawBody.trimStart();
|
|
102
|
+
if (!stripped.startsWith("/")) return null;
|
|
103
|
+
const token = stripped.split(/\s+/, 1)[0] ?? "";
|
|
104
|
+
const name = token.slice(1).replace(/_/g, "-").toLowerCase();
|
|
105
|
+
if (!name || name.includes("/")) return null;
|
|
106
|
+
return name;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isKnownOpenClawGroupSlashCommand(rawBody: string, cfg: OpenClawConfig): boolean {
|
|
110
|
+
const name = parseSlashCommandName(rawBody);
|
|
111
|
+
if (!name) return false;
|
|
112
|
+
return hasControlCommand(rawBody, cfg)
|
|
113
|
+
|| CLAWCHAT_PLUGIN_SLASH_COMMANDS.has(name)
|
|
114
|
+
|| OPENCLAW_CONFIRM_SLASH_COMMANDS.has(name);
|
|
115
|
+
}
|
|
37
116
|
|
|
38
117
|
export function getOpenclawClawlingClient(accountId: string): ClawlingChatClient | undefined {
|
|
39
118
|
return activeClients.get(accountId);
|
|
40
119
|
}
|
|
41
120
|
|
|
121
|
+
export function resolveClawChatMemoryRoot(
|
|
122
|
+
runtime: PluginRuntime,
|
|
123
|
+
cfg: OpenClawConfig,
|
|
124
|
+
agentId: string,
|
|
125
|
+
): string {
|
|
126
|
+
const resolver = runtime.agent?.resolveAgentWorkspaceDir;
|
|
127
|
+
if (typeof resolver !== "function") {
|
|
128
|
+
throw new Error(CLAWCHAT_MEMORY_ROOT_UNAVAILABLE);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let workspaceDir: string;
|
|
132
|
+
try {
|
|
133
|
+
workspaceDir = resolver(cfg, agentId);
|
|
134
|
+
} catch {
|
|
135
|
+
throw new Error(CLAWCHAT_MEMORY_ROOT_UNAVAILABLE);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const memoryRoot = typeof workspaceDir === "string" ? workspaceDir.trim() : "";
|
|
139
|
+
if (!memoryRoot) {
|
|
140
|
+
throw new Error(CLAWCHAT_MEMORY_ROOT_UNAVAILABLE);
|
|
141
|
+
}
|
|
142
|
+
return memoryRoot;
|
|
143
|
+
}
|
|
144
|
+
|
|
42
145
|
export async function waitForOpenclawClawlingClient(
|
|
43
146
|
accountId: string,
|
|
44
147
|
options: { timeoutMs?: number; pollMs?: number } = {},
|
|
@@ -112,6 +215,84 @@ function formatConversationSubject(peer: { kind: "direct" | "group"; id: string
|
|
|
112
215
|
return peer.kind === "group" ? `group:${peer.id}` : peer.id;
|
|
113
216
|
}
|
|
114
217
|
|
|
218
|
+
function parseApiTimestamp(value: unknown): number | null {
|
|
219
|
+
if (typeof value !== "string") return null;
|
|
220
|
+
const parsed = Date.parse(value);
|
|
221
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
225
|
+
return value && typeof value === "object" ? value as Record<string, unknown> : null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function optionalString(value: unknown): string | undefined {
|
|
229
|
+
return typeof value === "string" ? value : undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function hasOwn(record: Record<string, unknown>, key: string): boolean {
|
|
233
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function optionalNullableString(record: Record<string, unknown>, key: string): string | null | undefined {
|
|
237
|
+
if (!hasOwn(record, key)) return undefined;
|
|
238
|
+
const value = record[key];
|
|
239
|
+
if (value === null) return null;
|
|
240
|
+
return optionalString(value);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function isConversationNotFoundError(err: unknown): boolean {
|
|
244
|
+
if (!(err instanceof ClawlingApiError)) return false;
|
|
245
|
+
return err.meta?.status === 404 || err.meta?.status === 410 ||
|
|
246
|
+
err.meta?.code === 404 || err.meta?.code === 410 || err.meta?.code === 40401;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function metadataVersionFromEnvelope(env: Envelope): number | undefined {
|
|
250
|
+
const payload = asRecord(env.payload);
|
|
251
|
+
const version = payload?.version;
|
|
252
|
+
return typeof version === "number" && Number.isFinite(version) ? version : undefined;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function metadataScopesFromEnvelope(env: Envelope): string[] {
|
|
256
|
+
const scope = asRecord(env.payload)?.scope;
|
|
257
|
+
return Array.isArray(scope) ? scope.filter((item): item is string => typeof item === "string") : [];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function shouldRefreshBehaviorForScopes(scopes: string[]): boolean {
|
|
261
|
+
return scopes.includes("behavior");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function shouldRefreshConversationForScopes(scopes: string[]): boolean {
|
|
265
|
+
if (scopes.length === 0) return true;
|
|
266
|
+
return scopes.some((scope) => scope === "title" || scope === "description" || scope !== "behavior");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function buildConversationDetailsCacheInput(params: {
|
|
270
|
+
accountId: string;
|
|
271
|
+
conversation: ConversationDetails;
|
|
272
|
+
metadataVersion?: number;
|
|
273
|
+
}): Parameters<NonNullable<RuntimeConnectionStore["upsertConversationDetails"]>>[0] {
|
|
274
|
+
const { accountId, conversation, metadataVersion } = params;
|
|
275
|
+
const refreshedAt = Date.now();
|
|
276
|
+
const participants = Array.isArray(conversation.participants) ? conversation.participants : [];
|
|
277
|
+
return {
|
|
278
|
+
platform: "openclaw",
|
|
279
|
+
accountId,
|
|
280
|
+
conversationId: conversation.id,
|
|
281
|
+
conversationType: conversation.type,
|
|
282
|
+
...(metadataVersion !== undefined ? { metadataVersion } : {}),
|
|
283
|
+
lastSeenAt: parseApiTimestamp(conversation.updated_at),
|
|
284
|
+
lastRefreshedAt: refreshedAt,
|
|
285
|
+
raw: conversation,
|
|
286
|
+
members: participants.map((participant) => ({
|
|
287
|
+
userId: participant.user_id,
|
|
288
|
+
role: participant.role,
|
|
289
|
+
raw: participant,
|
|
290
|
+
lastSeenAt: parseApiTimestamp(participant.joined_at),
|
|
291
|
+
})),
|
|
292
|
+
membersComplete: true,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
115
296
|
function withClawChatSessionScope(cfg: OpenClawConfig): OpenClawConfig {
|
|
116
297
|
return {
|
|
117
298
|
...cfg,
|
|
@@ -122,6 +303,53 @@ function withClawChatSessionScope(cfg: OpenClawConfig): OpenClawConfig {
|
|
|
122
303
|
};
|
|
123
304
|
}
|
|
124
305
|
|
|
306
|
+
function buildActivationBootstrapText(): string {
|
|
307
|
+
return [
|
|
308
|
+
"ClawChat activation bootstrap: You are now connected to this ClawChat direct conversation.",
|
|
309
|
+
"Please do both:",
|
|
310
|
+
"1. Send a brief, friendly greeting to the user in this ClawChat direct conversation.",
|
|
311
|
+
"2. If you have local profile information for yourself, such as display name, bio, or avatar, update the connected ClawChat account profile using the available ClawChat tools. Use `clawchat_update_account_profile` for display name/bio/avatar URL, and use `clawchat_upload_avatar_image` first if the avatar is only available as a local image path. If you do not have local profile information, skip profile updates and only greet the user.",
|
|
312
|
+
"Do not ask the user for profile information just for this bootstrap.",
|
|
313
|
+
].join("\n");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function buildActivationBootstrapEnvelope(params: {
|
|
317
|
+
account: ResolvedOpenclawClawlingAccount;
|
|
318
|
+
conversationId: string;
|
|
319
|
+
}): Envelope {
|
|
320
|
+
const text = buildActivationBootstrapText();
|
|
321
|
+
const now = Date.now();
|
|
322
|
+
return {
|
|
323
|
+
version: "2",
|
|
324
|
+
event: EVENT.MESSAGE_SEND,
|
|
325
|
+
trace_id: `openclaw-clawchat-bootstrap-${now}`,
|
|
326
|
+
emitted_at: now,
|
|
327
|
+
chat_id: params.conversationId,
|
|
328
|
+
chat_type: "direct",
|
|
329
|
+
to: { id: params.account.userId, type: "direct" },
|
|
330
|
+
sender: {
|
|
331
|
+
id: "clawchat-bootstrap",
|
|
332
|
+
type: "direct",
|
|
333
|
+
nick_name: "ClawChat Activation",
|
|
334
|
+
},
|
|
335
|
+
payload: {
|
|
336
|
+
message_id: `openclaw-clawchat-bootstrap-${params.conversationId}-${now}`,
|
|
337
|
+
message_mode: "normal",
|
|
338
|
+
message: {
|
|
339
|
+
body: { fragments: [{ kind: "text", text }] },
|
|
340
|
+
context: { mentions: [], reply: null },
|
|
341
|
+
streaming: {
|
|
342
|
+
status: "static",
|
|
343
|
+
sequence: 0,
|
|
344
|
+
mutation_policy: "sealed",
|
|
345
|
+
started_at: null,
|
|
346
|
+
completed_at: null,
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
125
353
|
export interface StartGatewayParams {
|
|
126
354
|
cfg: OpenClawConfig;
|
|
127
355
|
account: ResolvedOpenclawClawlingAccount;
|
|
@@ -130,25 +358,59 @@ export interface StartGatewayParams {
|
|
|
130
358
|
getStatus: () => ChannelAccountSnapshot;
|
|
131
359
|
log?: Log;
|
|
132
360
|
/** Test hook only. */
|
|
361
|
+
store?: RuntimeConnectionStore | null;
|
|
362
|
+
/** Test hook only. */
|
|
133
363
|
transport?: Transport;
|
|
134
364
|
}
|
|
135
365
|
|
|
366
|
+
function resolveConnectionStore(
|
|
367
|
+
params: StartGatewayParams,
|
|
368
|
+
runtime: PluginRuntime,
|
|
369
|
+
): RuntimeConnectionStore | null {
|
|
370
|
+
if (params.store !== undefined) return params.store;
|
|
371
|
+
if (params.transport) return null;
|
|
372
|
+
try {
|
|
373
|
+
const stateDir = runtime.state?.resolveStateDir?.();
|
|
374
|
+
return getClawChatStore({
|
|
375
|
+
...(stateDir ? { dbPath: clawChatDbPathForStateDir(stateDir) } : {}),
|
|
376
|
+
log: { error: (message) => params.log?.error?.(message) },
|
|
377
|
+
});
|
|
378
|
+
} catch {
|
|
379
|
+
params.log?.error?.("openclaw-clawchat sqlite connection persistence unavailable; continuing.");
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
136
384
|
export async function startOpenclawClawlingGateway(params: StartGatewayParams): Promise<void> {
|
|
137
385
|
const { cfg, account, abortSignal, setStatus, getStatus, log } = params;
|
|
138
386
|
// Obtain PluginRuntime from the stored runtime set via setOpenclawClawlingRuntime.
|
|
139
387
|
const runtime = getOpenclawClawlingRuntime();
|
|
140
388
|
const accountId = account.accountId;
|
|
389
|
+
const store = resolveConnectionStore(params, runtime);
|
|
390
|
+
let conversationApiClient: ReturnType<typeof createOpenclawClawlingApiClient> | undefined;
|
|
391
|
+
const getConversationApiClient = () => {
|
|
392
|
+
conversationApiClient ??= createOpenclawClawlingApiClient({
|
|
393
|
+
baseUrl: account.baseUrl,
|
|
394
|
+
token: account.token,
|
|
395
|
+
userId: account.userId,
|
|
396
|
+
});
|
|
397
|
+
return conversationApiClient;
|
|
398
|
+
};
|
|
141
399
|
|
|
142
400
|
log?.info?.(
|
|
143
|
-
`[${accountId}] openclaw-clawchat runtime start entered configured=${account.configured} enabled=${account.enabled} hasToken=${Boolean(account.token)} hasUserId=${Boolean(account.userId)} websocketUrl=${account.websocketUrl || "(empty)"}`,
|
|
401
|
+
`[${accountId}] openclaw-clawchat runtime start entered configured=${account.configured} enabled=${account.enabled} hasToken=${Boolean(account.token)} hasUserId=${Boolean(account.userId)} hasOwnerUserId=${Boolean(account.ownerUserId)} websocketUrl=${account.websocketUrl || "(empty)"}`,
|
|
144
402
|
);
|
|
145
403
|
let lastHelloFailTraceId = "-";
|
|
146
404
|
let lastHelloFailReason = "";
|
|
147
405
|
let lastConnectTraceId = "-";
|
|
406
|
+
let lastHelloOkDeviceId: string | undefined;
|
|
407
|
+
let lastHelloOkDeliveryMode: string | undefined;
|
|
148
408
|
let currentAttemptStartedAt = 0;
|
|
149
409
|
let authFailureLogged = false;
|
|
150
410
|
let closingForAbort = false;
|
|
151
411
|
let wsReady = false;
|
|
412
|
+
let currentConnectionId: number | null = null;
|
|
413
|
+
let currentConnectionFinished = false;
|
|
152
414
|
const reconnectTracker = createReconnectTracker({
|
|
153
415
|
accountId,
|
|
154
416
|
log: (msg) => log?.info?.(msg),
|
|
@@ -162,11 +424,243 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
162
424
|
state: snapshot.state === "connected" ? "ready" : snapshot.state,
|
|
163
425
|
};
|
|
164
426
|
};
|
|
427
|
+
const recordConnection = <T>(action: string, fn: () => T): T | undefined => {
|
|
428
|
+
try {
|
|
429
|
+
return fn();
|
|
430
|
+
} catch {
|
|
431
|
+
log?.error?.(`[${accountId}] openclaw-clawchat sqlite ${action} failed; continuing`);
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
const finishCurrentConnection = (input: {
|
|
436
|
+
state: string;
|
|
437
|
+
disconnectedAt?: number;
|
|
438
|
+
closeCode?: number | null;
|
|
439
|
+
closeReason?: string | null;
|
|
440
|
+
error?: string | null;
|
|
441
|
+
}) => {
|
|
442
|
+
if (!store || currentConnectionId == null || currentConnectionFinished) return;
|
|
443
|
+
const connectionId = currentConnectionId;
|
|
444
|
+
recordConnection("finish", () => store.finishConnection(connectionId, input));
|
|
445
|
+
currentConnectionFinished = true;
|
|
446
|
+
currentConnectionId = null;
|
|
447
|
+
};
|
|
448
|
+
const refreshConversationDetails = async (
|
|
449
|
+
conversationId: string,
|
|
450
|
+
options: { metadataVersion?: number; source: string; memoryRoot?: string },
|
|
451
|
+
): Promise<void> => {
|
|
452
|
+
try {
|
|
453
|
+
const memoryRoot = options.memoryRoot;
|
|
454
|
+
const data = memoryRoot
|
|
455
|
+
? await (async () => {
|
|
456
|
+
const result = await pullGroupMetadata({
|
|
457
|
+
memoryRoot,
|
|
458
|
+
groupId: conversationId,
|
|
459
|
+
api: getConversationApiClient(),
|
|
460
|
+
});
|
|
461
|
+
if (result.failures.length > 0) {
|
|
462
|
+
log?.error?.(
|
|
463
|
+
`[${accountId}] openclaw-clawchat group participant metadata refresh partially failed: ${result.failures.map((failure) => `${failure.targetId}: ${failure.error}`).join("; ")}`,
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
if (!result.conversation) throw new Error("ClawChat conversation metadata response is missing conversation");
|
|
467
|
+
return { conversation: result.conversation };
|
|
468
|
+
})()
|
|
469
|
+
: await getConversationApiClient().getConversation(conversationId);
|
|
470
|
+
if (!store?.upsertConversationDetails) return;
|
|
471
|
+
recordConnection("conversation details upsert", () =>
|
|
472
|
+
store.upsertConversationDetails?.(buildConversationDetailsCacheInput({
|
|
473
|
+
accountId,
|
|
474
|
+
conversation: data.conversation,
|
|
475
|
+
...(options.metadataVersion !== undefined ? { metadataVersion: options.metadataVersion } : {}),
|
|
476
|
+
})),
|
|
477
|
+
);
|
|
478
|
+
} catch (err) {
|
|
479
|
+
if (isConversationNotFoundError(err)) {
|
|
480
|
+
if (store?.deleteConversationCache) {
|
|
481
|
+
recordConnection("conversation cache delete", () =>
|
|
482
|
+
store.deleteConversationCache?.({
|
|
483
|
+
platform: "openclaw",
|
|
484
|
+
accountId,
|
|
485
|
+
conversationId,
|
|
486
|
+
}),
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
log?.error?.(
|
|
492
|
+
`[${accountId}] openclaw-clawchat metadata refresh failed source=${options.source} conversation=${conversationId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
const refreshAgentBehavior = async (options: {
|
|
497
|
+
metadataVersion?: number;
|
|
498
|
+
source: string;
|
|
499
|
+
memoryRoot: string;
|
|
500
|
+
}): Promise<void> => {
|
|
501
|
+
try {
|
|
502
|
+
await pullOwnerMetadata({
|
|
503
|
+
memoryRoot: options.memoryRoot,
|
|
504
|
+
agentId: account.agentId,
|
|
505
|
+
accountUserId: account.userId,
|
|
506
|
+
accountOwnerUserId: account.ownerUserId,
|
|
507
|
+
api: getConversationApiClient(),
|
|
508
|
+
});
|
|
509
|
+
} catch (err) {
|
|
510
|
+
log?.error?.(
|
|
511
|
+
`[${accountId}] openclaw-clawchat behavior refresh failed source=${options.source} agent=${account.userId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
const refreshConversationCacheAfterReady = async (): Promise<void> => {
|
|
516
|
+
if (!store) return;
|
|
517
|
+
const ids: string[] = [];
|
|
518
|
+
const seen = new Set<string>();
|
|
519
|
+
const addId = (id: unknown) => {
|
|
520
|
+
if (typeof id !== "string" || !id || seen.has(id)) return;
|
|
521
|
+
seen.add(id);
|
|
522
|
+
ids.push(id);
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const activation = store.getActivationConversation
|
|
526
|
+
? recordConnection("activation conversation read", () =>
|
|
527
|
+
store.getActivationConversation?.({ platform: "openclaw", accountId }),
|
|
528
|
+
)
|
|
529
|
+
: null;
|
|
530
|
+
addId(activation?.conversationId);
|
|
531
|
+
|
|
532
|
+
const cachedIds = store.listCachedConversationIds
|
|
533
|
+
? recordConnection("cached conversation ids read", () =>
|
|
534
|
+
store.listCachedConversationIds?.({ platform: "openclaw", accountId, limit: 20 }),
|
|
535
|
+
) ?? []
|
|
536
|
+
: [];
|
|
537
|
+
for (const id of cachedIds.slice(0, 20)) addId(id);
|
|
538
|
+
|
|
539
|
+
for (const id of ids) {
|
|
540
|
+
await refreshConversationDetails(id, { source: "reconnect" });
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
const resolveMemoryRootForPeer = (peer: { kind: "direct" | "group"; id: string }): string | null => {
|
|
544
|
+
try {
|
|
545
|
+
const route = runtime.channel.routing.resolveAgentRoute({
|
|
546
|
+
cfg: withClawChatSessionScope(cfg),
|
|
547
|
+
channel: CHANNEL_ID,
|
|
548
|
+
accountId,
|
|
549
|
+
peer,
|
|
550
|
+
});
|
|
551
|
+
return resolveClawChatMemoryRoot(runtime, cfg, route.agentId);
|
|
552
|
+
} catch (err) {
|
|
553
|
+
log?.error?.(
|
|
554
|
+
`[${accountId}] openclaw-clawchat metadata refresh memory root unavailable: ${err instanceof Error ? err.message : String(err)}`,
|
|
555
|
+
);
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
const handleMetadataInvalidation = async (env: Envelope): Promise<void> => {
|
|
560
|
+
const conversationId = typeof env.chat_id === "string" && env.chat_id.trim()
|
|
561
|
+
? env.chat_id
|
|
562
|
+
: "";
|
|
563
|
+
if (!conversationId) {
|
|
564
|
+
log?.info?.(`[${accountId}] openclaw-clawchat metadata invalidation missing chat_id trace=${env.trace_id}`);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const version = metadataVersionFromEnvelope(env);
|
|
569
|
+
const scopes = metadataScopesFromEnvelope(env);
|
|
570
|
+
const refreshBehavior = shouldRefreshBehaviorForScopes(scopes);
|
|
571
|
+
const refreshConversation = shouldRefreshConversationForScopes(scopes);
|
|
572
|
+
if (!refreshBehavior && !refreshConversation) return;
|
|
573
|
+
const peer = {
|
|
574
|
+
kind: env.chat_type === "group" ? "group" as const : "direct" as const,
|
|
575
|
+
id: conversationId,
|
|
576
|
+
};
|
|
577
|
+
const memoryRoot = resolveMemoryRootForPeer(peer);
|
|
578
|
+
if (!memoryRoot) return;
|
|
579
|
+
|
|
580
|
+
if (refreshBehavior) {
|
|
581
|
+
await refreshAgentBehavior({
|
|
582
|
+
source: "metadata_invalidation",
|
|
583
|
+
...(version !== undefined ? { metadataVersion: version } : {}),
|
|
584
|
+
memoryRoot,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (!refreshConversation) return;
|
|
589
|
+
|
|
590
|
+
await refreshConversationDetails(conversationId, {
|
|
591
|
+
source: "metadata_invalidation",
|
|
592
|
+
...(version !== undefined ? { metadataVersion: version } : {}),
|
|
593
|
+
memoryRoot,
|
|
594
|
+
});
|
|
595
|
+
};
|
|
596
|
+
const syncMessagePathProfiles = async (turn: IngestTurnParams, memoryRoot: string): Promise<void> => {
|
|
597
|
+
if (turn.senderId === "clawchat-bootstrap") {
|
|
598
|
+
await pullOwnerMetadata({
|
|
599
|
+
memoryRoot,
|
|
600
|
+
agentId: account.agentId,
|
|
601
|
+
accountUserId: account.userId,
|
|
602
|
+
accountOwnerUserId: account.ownerUserId,
|
|
603
|
+
api: getConversationApiClient(),
|
|
604
|
+
});
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (turn.peer.kind === "direct" && (turn.senderId === account.ownerUserId || turn.senderId === account.userId)) {
|
|
608
|
+
await pullOwnerMetadata({
|
|
609
|
+
memoryRoot,
|
|
610
|
+
agentId: account.agentId,
|
|
611
|
+
accountUserId: account.userId,
|
|
612
|
+
accountOwnerUserId: account.ownerUserId,
|
|
613
|
+
api: getConversationApiClient(),
|
|
614
|
+
});
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (turn.peer.kind === "group") {
|
|
618
|
+
await refreshGroupProfile({
|
|
619
|
+
platform: "openclaw",
|
|
620
|
+
accountId,
|
|
621
|
+
conversationId: turn.peer.id,
|
|
622
|
+
api: getConversationApiClient(),
|
|
623
|
+
store: {
|
|
624
|
+
...(store?.upsertConversationDetails ? { upsertConversationDetails: store.upsertConversationDetails.bind(store) } : {}),
|
|
625
|
+
},
|
|
626
|
+
memoryRoot,
|
|
627
|
+
log: { error: (message) => log?.error?.(`[${accountId}] ${message}`) },
|
|
628
|
+
});
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
await syncFirstSeenClawChatProfiles({
|
|
632
|
+
platform: "openclaw",
|
|
633
|
+
accountId,
|
|
634
|
+
accountUserId: account.userId,
|
|
635
|
+
accountOwnerUserId: account.ownerUserId,
|
|
636
|
+
chat: {
|
|
637
|
+
id: turn.peer.id,
|
|
638
|
+
type: "direct",
|
|
639
|
+
lastSeenAt: turn.timestamp,
|
|
640
|
+
},
|
|
641
|
+
sender: {
|
|
642
|
+
id: turn.senderId,
|
|
643
|
+
...(turn.senderNickName ? { nickname: turn.senderNickName } : {}),
|
|
644
|
+
},
|
|
645
|
+
api: getConversationApiClient(),
|
|
646
|
+
store: {
|
|
647
|
+
...(store?.getCachedConversation ? { getCachedConversation: store.getCachedConversation.bind(store) } : {}),
|
|
648
|
+
...(store?.upsertConversationSummary ? { upsertConversationSummary: store.upsertConversationSummary.bind(store) } : {}),
|
|
649
|
+
...(store?.upsertConversationDetails ? { upsertConversationDetails: store.upsertConversationDetails.bind(store) } : {}),
|
|
650
|
+
},
|
|
651
|
+
memoryRoot,
|
|
652
|
+
log: { error: (message) => log?.error?.(`[${accountId}] ${message}`) },
|
|
653
|
+
});
|
|
654
|
+
};
|
|
165
655
|
const client = createOpenclawClawlingClient(account, {
|
|
166
656
|
...(params.transport ? { transport: params.transport } : {}),
|
|
167
657
|
wsLifecycle: {
|
|
168
658
|
onConnectFrameSent: (env) => {
|
|
169
659
|
lastConnectTraceId = typeof env.trace_id === "string" ? env.trace_id : "-";
|
|
660
|
+
if (store && currentConnectionId != null) {
|
|
661
|
+
const connectionId = currentConnectionId;
|
|
662
|
+
recordConnection("connect-sent", () => store.markConnectSent(connectionId));
|
|
663
|
+
}
|
|
170
664
|
const deviceId =
|
|
171
665
|
typeof env.payload?.device_id === "string" ? env.payload.device_id : "-";
|
|
172
666
|
const current = wsLogContext();
|
|
@@ -190,6 +684,13 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
190
684
|
log?.info?.(`[${accountId}] openclaw-clawchat runtime client created`);
|
|
191
685
|
|
|
192
686
|
setAlignedOutboundLogContext(client, wsLogContext);
|
|
687
|
+
client.on("hello:ok", (env: Envelope) => {
|
|
688
|
+
const payload = env.payload && typeof env.payload === "object"
|
|
689
|
+
? env.payload as { device_id?: unknown; delivery_mode?: unknown }
|
|
690
|
+
: {};
|
|
691
|
+
lastHelloOkDeviceId = typeof payload.device_id === "string" ? payload.device_id : undefined;
|
|
692
|
+
lastHelloOkDeliveryMode = typeof payload.delivery_mode === "string" ? payload.delivery_mode : undefined;
|
|
693
|
+
});
|
|
193
694
|
const protocolControlLogger = createProtocolControlHandler({
|
|
194
695
|
accountId,
|
|
195
696
|
log: (msg) => log?.info?.(msg),
|
|
@@ -215,6 +716,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
215
716
|
}),
|
|
216
717
|
);
|
|
217
718
|
};
|
|
719
|
+
let dispatchActivationBootstrap: () => Promise<void> = async () => {};
|
|
218
720
|
|
|
219
721
|
client.on("state", ({ from, to }) => {
|
|
220
722
|
log?.info?.(`[${accountId}] openclaw-clawchat state ${from} -> ${to}`);
|
|
@@ -223,6 +725,18 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
223
725
|
reconnectTracker.connectStart();
|
|
224
726
|
currentAttemptStartedAt = Date.now();
|
|
225
727
|
const current = wsLogContext();
|
|
728
|
+
if (store) {
|
|
729
|
+
recordConnection("start", () => {
|
|
730
|
+
currentConnectionId = store.startConnection({
|
|
731
|
+
platform: "openclaw",
|
|
732
|
+
accountId,
|
|
733
|
+
attempt: current.attempt,
|
|
734
|
+
reconnectCount: current.reconnectCount,
|
|
735
|
+
connectStartedAt: currentAttemptStartedAt,
|
|
736
|
+
});
|
|
737
|
+
currentConnectionFinished = false;
|
|
738
|
+
});
|
|
739
|
+
}
|
|
226
740
|
log?.info?.(
|
|
227
741
|
formatWsLog({
|
|
228
742
|
event: "connect_start",
|
|
@@ -242,6 +756,19 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
242
756
|
const queueSize = getAlignedOutboundQueueSize(client);
|
|
243
757
|
reconnectTracker.markReady();
|
|
244
758
|
const current = wsLogContext();
|
|
759
|
+
if (store && currentConnectionId != null) {
|
|
760
|
+
const connectionId = currentConnectionId;
|
|
761
|
+
recordConnection("ready", () => {
|
|
762
|
+
if (lastHelloOkDeviceId === undefined && lastHelloOkDeliveryMode === undefined) {
|
|
763
|
+
store.markConnectionReady(connectionId);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
store.markConnectionReady(connectionId, {
|
|
767
|
+
...(lastHelloOkDeviceId !== undefined ? { resolvedDeviceId: lastHelloOkDeviceId } : {}),
|
|
768
|
+
...(lastHelloOkDeliveryMode !== undefined ? { deliveryMode: lastHelloOkDeliveryMode } : {}),
|
|
769
|
+
});
|
|
770
|
+
});
|
|
771
|
+
}
|
|
245
772
|
log?.info?.(
|
|
246
773
|
formatWsLog({
|
|
247
774
|
event: "handshake_ok",
|
|
@@ -262,6 +789,8 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
262
789
|
} catch {
|
|
263
790
|
// The queue keeps the failed frame at the head and will retry after the next reconnect.
|
|
264
791
|
}
|
|
792
|
+
void refreshConversationCacheAfterReady();
|
|
793
|
+
void dispatchActivationBootstrap();
|
|
265
794
|
} else if (to === "disconnected") {
|
|
266
795
|
reconnectTracker.markClosed();
|
|
267
796
|
}
|
|
@@ -271,6 +800,11 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
271
800
|
|
|
272
801
|
client.on("close", ({ code, reason }: { code?: number; reason?: string }) => {
|
|
273
802
|
if (closingForAbort || (code === 1000 && reason === "client close")) return;
|
|
803
|
+
finishCurrentConnection({
|
|
804
|
+
state: "disconnected",
|
|
805
|
+
closeCode: code ?? null,
|
|
806
|
+
closeReason: reason ?? null,
|
|
807
|
+
});
|
|
274
808
|
const current = wsLogContext();
|
|
275
809
|
log?.info?.(
|
|
276
810
|
formatWsLog({
|
|
@@ -398,9 +932,17 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
398
932
|
lastHelloFailReason = typeof payload?.reason === "string" ? payload.reason : "";
|
|
399
933
|
});
|
|
400
934
|
|
|
935
|
+
client.on("metadata:invalidated", (env: Envelope) => {
|
|
936
|
+
void handleMetadataInvalidation(env);
|
|
937
|
+
});
|
|
938
|
+
|
|
401
939
|
client.on("error", (err: unknown) => {
|
|
402
940
|
const classified = classifyClawlingClientError(err);
|
|
403
941
|
if (classified.kind === "auth") {
|
|
942
|
+
finishCurrentConnection({
|
|
943
|
+
state: "auth_failed",
|
|
944
|
+
error: lastHelloFailReason || classified.message,
|
|
945
|
+
});
|
|
404
946
|
logAuthFailure(classified.message);
|
|
405
947
|
setStatus({
|
|
406
948
|
...getStatus(),
|
|
@@ -410,6 +952,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
410
952
|
lastError: classified.message,
|
|
411
953
|
});
|
|
412
954
|
} else if (classified.kind === "transport") {
|
|
955
|
+
finishCurrentConnection({ state: "transport_error", error: classified.message });
|
|
413
956
|
const current = wsLogContext();
|
|
414
957
|
log?.info?.(
|
|
415
958
|
formatWsLog({
|
|
@@ -436,153 +979,428 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
436
979
|
} else if (classified.kind === "state") {
|
|
437
980
|
log?.info?.(`[${accountId}] openclaw-clawchat state error: ${classified.message}`);
|
|
438
981
|
} else {
|
|
439
|
-
log?.error?.(`[${accountId}] openclaw-clawchat
|
|
982
|
+
log?.error?.(`[${accountId}] openclaw-clawchat client error: ${classified.message}`);
|
|
440
983
|
}
|
|
441
984
|
});
|
|
442
985
|
|
|
443
|
-
|
|
986
|
+
type IngestTurnResult = "submitted" | "skipped" | "failed";
|
|
987
|
+
type GroupIngestTurnParams = IngestTurnParams & { peer: { kind: "group"; id: string } };
|
|
988
|
+
|
|
989
|
+
const buildPromptForTurn = async (turn: IngestTurnParams, memoryRoot: string): Promise<string> => {
|
|
990
|
+
const senderRelation = resolveSenderRelation({
|
|
991
|
+
senderId: turn.senderId,
|
|
992
|
+
accountUserId: account.userId,
|
|
993
|
+
accountOwnerUserId: account.ownerUserId,
|
|
994
|
+
senderProfileType: turn.senderProfileType,
|
|
995
|
+
});
|
|
996
|
+
const promptChatType = turn.peer.kind === "group" ? "group" : "dm";
|
|
997
|
+
const promptMetadata = await loadClawChatPromptMetadata({
|
|
998
|
+
memoryRoot,
|
|
999
|
+
turn: {
|
|
1000
|
+
chatType: promptChatType,
|
|
1001
|
+
senderId: turn.senderId,
|
|
1002
|
+
senderIsOwner: senderRelation === "owner",
|
|
1003
|
+
groupId: turn.peer.kind === "group" ? turn.peer.id : null,
|
|
1004
|
+
},
|
|
1005
|
+
log: { error: (message) => log?.error?.(`[${accountId}] ${message}`) },
|
|
1006
|
+
});
|
|
1007
|
+
const metadataSenderProfileType = promptMetadata.userMetadata?.profile_type ?? null;
|
|
1008
|
+
const promptSenderProfileType = metadataSenderProfileType ?? turn.senderProfileType ?? (
|
|
1009
|
+
senderRelation === "self_agent" || senderRelation === "peer_agent" ? "agent" : "user"
|
|
1010
|
+
);
|
|
1011
|
+
return renderClawChatProfilePrompt({
|
|
1012
|
+
basePrompt: turn.peer.kind === "group" ? getClawChatGroupPrompt() : getClawChatUserPrompt(),
|
|
1013
|
+
...promptMetadata,
|
|
1014
|
+
turn: {
|
|
1015
|
+
chatType: promptChatType,
|
|
1016
|
+
senderId: turn.senderId,
|
|
1017
|
+
senderName: promptMetadata.userMetadata?.nickname ?? (turn.senderNickName || turn.senderId),
|
|
1018
|
+
senderProfileType: promptSenderProfileType,
|
|
1019
|
+
senderIsOwner: senderRelation === "owner",
|
|
1020
|
+
groupId: turn.peer.kind === "group" ? turn.peer.id : null,
|
|
1021
|
+
coalescedGroupBatch: turn.coalescedGroupBatch === true,
|
|
1022
|
+
wasMentioned: turn.wasMentioned,
|
|
1023
|
+
mentionedUserIds: turn.mentionedUserIds,
|
|
1024
|
+
},
|
|
1025
|
+
});
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
const resolveSenderNickNameForTurn = (turn: IngestTurnParams): string => {
|
|
1029
|
+
if (turn.senderNickName && turn.senderNickName !== turn.senderId) return turn.senderNickName;
|
|
1030
|
+
return turn.senderNickName || turn.senderId;
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
const resolveSenderBatchIdentityForTurn = (turn: IngestTurnParams): {
|
|
1034
|
+
senderRelation: NonNullable<IngestTurnParams["senderRelation"]>;
|
|
1035
|
+
senderProfileType: string;
|
|
1036
|
+
senderIsOwner: boolean;
|
|
1037
|
+
} => {
|
|
1038
|
+
const senderRelation = resolveSenderRelation({
|
|
1039
|
+
senderId: turn.senderId,
|
|
1040
|
+
accountUserId: account.userId,
|
|
1041
|
+
accountOwnerUserId: account.ownerUserId,
|
|
1042
|
+
senderProfileType: turn.senderProfileType,
|
|
1043
|
+
});
|
|
1044
|
+
return {
|
|
1045
|
+
senderRelation,
|
|
1046
|
+
senderProfileType: turn.senderProfileType ??
|
|
1047
|
+
(senderRelation === "self_agent" || senderRelation === "peer_agent" ? "agent" : "user"),
|
|
1048
|
+
senderIsOwner: senderRelation === "owner",
|
|
1049
|
+
};
|
|
1050
|
+
};
|
|
1051
|
+
|
|
1052
|
+
const claimInboundTurn = (turn: IngestTurnParams): "claimed" | "skipped" => {
|
|
1053
|
+
const env = turn.envelope;
|
|
1054
|
+
if (store?.claimMessageOnce) {
|
|
1055
|
+
const claimed = recordConnection("message claim", () =>
|
|
1056
|
+
store.claimMessageOnce?.({
|
|
1057
|
+
platform: "openclaw",
|
|
1058
|
+
accountId,
|
|
1059
|
+
kind: "message",
|
|
1060
|
+
direction: "inbound",
|
|
1061
|
+
eventType: String(env.event),
|
|
1062
|
+
traceId: turn.traceId,
|
|
1063
|
+
chatId: turn.peer.id,
|
|
1064
|
+
messageId: turn.messageId,
|
|
1065
|
+
text: turn.rawBody,
|
|
1066
|
+
raw: env,
|
|
1067
|
+
}),
|
|
1068
|
+
);
|
|
1069
|
+
if (claimed === false) {
|
|
1070
|
+
log?.info?.(
|
|
1071
|
+
`[${accountId}] openclaw-clawchat skip duplicate stored msg=${turn.messageId}`,
|
|
1072
|
+
);
|
|
1073
|
+
return "skipped";
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return "claimed";
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
const dispatchTurnToAgent = async (turn: IngestTurnParams): Promise<IngestTurnResult> => {
|
|
1080
|
+
const rt = runtime.channel;
|
|
1081
|
+
const storePath = rt.session.resolveStorePath(cfg.session?.store);
|
|
1082
|
+
const routeCfg = withClawChatSessionScope(cfg);
|
|
1083
|
+
const route = rt.routing.resolveAgentRoute({
|
|
1084
|
+
cfg: routeCfg,
|
|
1085
|
+
channel: CHANNEL_ID,
|
|
1086
|
+
accountId,
|
|
1087
|
+
peer: turn.peer,
|
|
1088
|
+
});
|
|
1089
|
+
const memoryRoot = resolveClawChatMemoryRoot(runtime, cfg, route.agentId);
|
|
1090
|
+
const body = rt.reply.formatAgentEnvelope({
|
|
1091
|
+
channel: "Clawling Chat",
|
|
1092
|
+
from: formatConversationSubject(turn.peer),
|
|
1093
|
+
body: turn.rawBody,
|
|
1094
|
+
timestamp: turn.timestamp,
|
|
1095
|
+
...rt.reply.resolveEnvelopeFormatOptions(cfg),
|
|
1096
|
+
});
|
|
444
1097
|
try {
|
|
445
|
-
await
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
1098
|
+
await syncMessagePathProfiles(turn, memoryRoot);
|
|
1099
|
+
} catch (err) {
|
|
1100
|
+
log?.error?.(
|
|
1101
|
+
`[${accountId}] openclaw-clawchat message metadata refresh failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
const conversationTarget = `${CHANNEL_ID}:${formatConversationSubject(turn.peer)}`;
|
|
1105
|
+
const turnPrompt = await buildPromptForTurn(turn, memoryRoot);
|
|
1106
|
+
const ctxPayload = rt.turn.buildContext({
|
|
1107
|
+
channel: CHANNEL_ID,
|
|
1108
|
+
accountId: route.accountId ?? accountId,
|
|
1109
|
+
provider: CHANNEL_ID,
|
|
1110
|
+
surface: CHANNEL_ID,
|
|
1111
|
+
messageId: turn.messageId,
|
|
1112
|
+
messageIdFull: turn.messageId,
|
|
1113
|
+
timestamp: turn.timestamp,
|
|
1114
|
+
from: conversationTarget,
|
|
1115
|
+
sender: {
|
|
1116
|
+
id: turn.senderId,
|
|
1117
|
+
name: turn.senderNickName || turn.senderId,
|
|
1118
|
+
displayLabel: turn.senderNickName || turn.senderId,
|
|
1119
|
+
},
|
|
1120
|
+
conversation: {
|
|
1121
|
+
kind: turn.peer.kind,
|
|
1122
|
+
id: turn.peer.id,
|
|
1123
|
+
label: formatConversationSubject(turn.peer),
|
|
1124
|
+
routePeer: turn.peer,
|
|
1125
|
+
},
|
|
1126
|
+
route: {
|
|
1127
|
+
agentId: route.agentId,
|
|
1128
|
+
accountId: route.accountId ?? accountId,
|
|
1129
|
+
routeSessionKey: route.sessionKey,
|
|
1130
|
+
},
|
|
1131
|
+
reply: {
|
|
1132
|
+
to: `${CHANNEL_ID}:${account.userId}`,
|
|
1133
|
+
originatingTo: conversationTarget,
|
|
1134
|
+
},
|
|
1135
|
+
message: {
|
|
1136
|
+
body,
|
|
1137
|
+
rawBody: turn.rawBody,
|
|
1138
|
+
bodyForAgent: turn.rawBody,
|
|
1139
|
+
commandBody: turn.rawBody,
|
|
1140
|
+
envelopeFrom: conversationTarget,
|
|
1141
|
+
},
|
|
1142
|
+
access: {
|
|
1143
|
+
mentions: {
|
|
1144
|
+
canDetectMention: true,
|
|
1145
|
+
wasMentioned: turn.wasMentioned,
|
|
1146
|
+
hasAnyMention: turn.mentionedUserIds.length > 0,
|
|
1147
|
+
},
|
|
1148
|
+
},
|
|
1149
|
+
...(memoryRoot ? { extra: { memoryRoot } } : {}),
|
|
1150
|
+
...(turn.peer.kind === "group"
|
|
1151
|
+
? { supplemental: { groupSystemPrompt: turnPrompt } }
|
|
1152
|
+
: {}),
|
|
1153
|
+
});
|
|
1154
|
+
if (memoryRoot) {
|
|
1155
|
+
(ctxPayload as Record<string, unknown>).memoryRoot = memoryRoot;
|
|
1156
|
+
}
|
|
1157
|
+
if (turn.mentionedUserIds.length > 0) {
|
|
1158
|
+
(ctxPayload as Record<string, unknown>).MentionedUserIds = turn.mentionedUserIds;
|
|
1159
|
+
}
|
|
1160
|
+
// Fetch any inbound media attachments and populate MediaPath/MediaPaths in context.
|
|
1161
|
+
const inboundPaths =
|
|
1162
|
+
turn.mediaItems.length > 0
|
|
1163
|
+
? await fetchInboundMedia(turn.mediaItems, {
|
|
1164
|
+
runtime,
|
|
1165
|
+
log,
|
|
1166
|
+
maxBytes: 20 * 1024 * 1024,
|
|
1167
|
+
})
|
|
1168
|
+
: [];
|
|
1169
|
+
if (inboundPaths.length > 0) {
|
|
1170
|
+
(ctxPayload as Record<string, unknown>).MediaPath = inboundPaths[0];
|
|
1171
|
+
(ctxPayload as Record<string, unknown>).MediaPaths = inboundPaths;
|
|
1172
|
+
}
|
|
505
1173
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
1174
|
+
const resolvedSessionKey = ctxPayload.SessionKey ?? route.sessionKey;
|
|
1175
|
+
|
|
1176
|
+
clearClawChatPromptInjectionForSession(resolvedSessionKey);
|
|
1177
|
+
const stagedDirectPrompt = turn.peer.kind !== "group";
|
|
1178
|
+
if (stagedDirectPrompt) {
|
|
1179
|
+
stageClawChatPromptInjection({
|
|
1180
|
+
sessionKey: resolvedSessionKey,
|
|
1181
|
+
prompt: turnPrompt,
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
try {
|
|
1186
|
+
try {
|
|
1187
|
+
await rt.session.recordInboundSession({
|
|
1188
|
+
storePath,
|
|
1189
|
+
sessionKey: resolvedSessionKey,
|
|
1190
|
+
ctx: ctxPayload,
|
|
1191
|
+
onRecordError: (err) => {
|
|
518
1192
|
log?.error?.(
|
|
519
1193
|
`[${accountId}] openclaw-clawchat failed to record inbound session: ${String(err)}`,
|
|
520
1194
|
);
|
|
521
|
-
}
|
|
1195
|
+
},
|
|
1196
|
+
});
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
log?.error?.(
|
|
1199
|
+
`[${accountId}] openclaw-clawchat failed to record inbound session: ${String(err)}`,
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
522
1202
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
1203
|
+
const replyCtx = turn.replyCtx;
|
|
1204
|
+
const { dispatcher, replyOptions, markDispatchIdle } =
|
|
1205
|
+
createOpenclawClawlingReplyDispatcher({
|
|
1206
|
+
cfg,
|
|
1207
|
+
runtime,
|
|
1208
|
+
account,
|
|
1209
|
+
client,
|
|
1210
|
+
target: { chatId: turn.peer.id, chatType: turn.peer.kind },
|
|
1211
|
+
...(replyCtx ? { replyCtx } : {}),
|
|
1212
|
+
inboundMessageId: turn.messageId,
|
|
1213
|
+
inboundForFinalReply: {
|
|
1214
|
+
chatId: turn.peer.id,
|
|
1215
|
+
senderId: turn.senderId,
|
|
1216
|
+
senderNickName: turn.senderNickName || turn.senderId,
|
|
1217
|
+
bodyText: turn.rawBody,
|
|
1218
|
+
},
|
|
1219
|
+
store: store
|
|
1220
|
+
? {
|
|
1221
|
+
insertMessage: (input) => store.insertMessage?.(input) ?? null,
|
|
1222
|
+
claimMessageOnce: (input) => store.claimMessageOnce?.(input) ?? null,
|
|
1223
|
+
updateMessageByIdentity: (input) => store.updateMessageByIdentity?.(input),
|
|
1224
|
+
}
|
|
1225
|
+
: null,
|
|
1226
|
+
log,
|
|
1227
|
+
});
|
|
541
1228
|
|
|
542
|
-
|
|
1229
|
+
const agentsConfigured = Object.keys((cfg as { agents?: Record<string, unknown> }).agents ?? {});
|
|
1230
|
+
log?.info?.(
|
|
1231
|
+
`[${accountId}] openclaw-clawchat dispatching reply msg=${turn.messageId} session=${resolvedSessionKey} agent=${route.agentId} agentsConfigured=[${agentsConfigured.join(",")}]`,
|
|
1232
|
+
);
|
|
1233
|
+
|
|
1234
|
+
try {
|
|
1235
|
+
const dispatchResult = await rt.reply.withReplyDispatcher({
|
|
1236
|
+
dispatcher,
|
|
1237
|
+
onSettled: () => markDispatchIdle(),
|
|
1238
|
+
run: () => rt.reply.dispatchReplyFromConfig({ ctx: ctxPayload, cfg, dispatcher, replyOptions }),
|
|
1239
|
+
});
|
|
1240
|
+
const counts = (dispatchResult as { counts?: Record<string, number> } | undefined)?.counts ?? {};
|
|
1241
|
+
const queuedFinal = Boolean(
|
|
1242
|
+
(dispatchResult as { queuedFinal?: boolean } | undefined)?.queuedFinal,
|
|
1243
|
+
);
|
|
1244
|
+
log?.info?.(
|
|
1245
|
+
`[${accountId}] openclaw-clawchat dispatch complete msg=${turn.messageId} queuedFinal=${queuedFinal} counts=${JSON.stringify(counts)}`,
|
|
1246
|
+
);
|
|
1247
|
+
if (!queuedFinal && Object.values(counts).every((n) => !n)) {
|
|
543
1248
|
log?.info?.(
|
|
544
|
-
`[${accountId}] openclaw-clawchat
|
|
1249
|
+
`[${accountId}] openclaw-clawchat NO reply was produced (no final / block / tool dispatched). ` +
|
|
1250
|
+
`Likely causes: agent='${route.agentId}' not configured in cfg.agents (configured: [${agentsConfigured.join(",")}]); ` +
|
|
1251
|
+
`or send-policy denied; or a plugin claimed the binding.`,
|
|
545
1252
|
);
|
|
1253
|
+
}
|
|
1254
|
+
return "submitted";
|
|
1255
|
+
} catch (err) {
|
|
1256
|
+
log?.error?.(
|
|
1257
|
+
`[${accountId}] openclaw-clawchat dispatch failed msg=${turn.messageId}: ${String(err)}`,
|
|
1258
|
+
);
|
|
1259
|
+
return "failed";
|
|
1260
|
+
}
|
|
1261
|
+
} finally {
|
|
1262
|
+
if (stagedDirectPrompt) {
|
|
1263
|
+
clearClawChatPromptInjectionForSession(resolvedSessionKey);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
546
1267
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
1268
|
+
const groupCoalescer = createGroupMessageCoalescer<GroupIngestTurnParams>({
|
|
1269
|
+
idleMs: 10_000,
|
|
1270
|
+
maxWaitMs: 30_000,
|
|
1271
|
+
dispatch: async (turn) => {
|
|
1272
|
+
await dispatchTurnToAgent(turn);
|
|
1273
|
+
},
|
|
1274
|
+
onError: (err) => {
|
|
1275
|
+
log?.error?.(
|
|
1276
|
+
`[${accountId}] openclaw-clawchat coalesced group dispatch failed: ${String(err)}`,
|
|
1277
|
+
);
|
|
1278
|
+
},
|
|
1279
|
+
onDrop: (chatId, count) => {
|
|
1280
|
+
log?.info?.(
|
|
1281
|
+
`[${accountId}] openclaw-clawchat dropped pending group batch chat_id=${chatId} count=${count} reason=shutdown`,
|
|
1282
|
+
);
|
|
1283
|
+
},
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
const ingestTurn = async (rawTurn: IngestTurnParams): Promise<IngestTurnResult> => {
|
|
1287
|
+
const senderBatchIdentity = rawTurn.peer.kind === "group"
|
|
1288
|
+
? resolveSenderBatchIdentityForTurn(rawTurn)
|
|
1289
|
+
: {};
|
|
1290
|
+
const turn: IngestTurnParams = {
|
|
1291
|
+
...rawTurn,
|
|
1292
|
+
senderNickName: resolveSenderNickNameForTurn(rawTurn),
|
|
1293
|
+
...senderBatchIdentity,
|
|
1294
|
+
};
|
|
1295
|
+
const claimed = claimInboundTurn(turn);
|
|
1296
|
+
if (claimed === "skipped") return "skipped";
|
|
1297
|
+
if (turn.peer.kind === "group") {
|
|
1298
|
+
if (isKnownOpenClawGroupSlashCommand(turn.rawBody, cfg)) {
|
|
1299
|
+
const commandMode = effectiveGroupCommandMode(account, turn.peer.id);
|
|
1300
|
+
const commandAllowed = commandMode === "all"
|
|
1301
|
+
|| (commandMode === "owner" && turn.senderIsOwner === true);
|
|
1302
|
+
if (!commandAllowed) {
|
|
1303
|
+
log?.info?.(
|
|
1304
|
+
`[${accountId}] openclaw-clawchat group command dropped chat_id=${turn.peer.id} msg=${turn.messageId} mode=${commandMode} owner=${turn.senderIsOwner === true}`,
|
|
1305
|
+
);
|
|
1306
|
+
return "skipped";
|
|
1307
|
+
}
|
|
1308
|
+
log?.info?.(
|
|
1309
|
+
`[${accountId}] openclaw-clawchat dispatching group command chat_id=${turn.peer.id} msg=${turn.messageId} mode=${commandMode}`,
|
|
1310
|
+
);
|
|
1311
|
+
return dispatchTurnToAgent(turn);
|
|
1312
|
+
}
|
|
1313
|
+
if (turn.wasMentioned) {
|
|
1314
|
+
groupCoalescer.enqueue(turn as GroupIngestTurnParams);
|
|
1315
|
+
groupCoalescer.flushNow(turn.peer.id);
|
|
1316
|
+
log?.info?.(
|
|
1317
|
+
`[${accountId}] openclaw-clawchat dispatching mentioned group batch chat_id=${turn.peer.id} msg=${turn.messageId}`,
|
|
1318
|
+
);
|
|
1319
|
+
return "submitted";
|
|
1320
|
+
}
|
|
1321
|
+
groupCoalescer.enqueue(turn as GroupIngestTurnParams);
|
|
1322
|
+
log?.info?.(
|
|
1323
|
+
`[${accountId}] openclaw-clawchat queued group batch chat_id=${turn.peer.id} msg=${turn.messageId}`,
|
|
1324
|
+
);
|
|
1325
|
+
return "submitted";
|
|
1326
|
+
}
|
|
1327
|
+
return dispatchTurnToAgent(turn);
|
|
1328
|
+
};
|
|
1329
|
+
|
|
1330
|
+
const handleInboundEnvelope = async (env: Envelope): Promise<IngestTurnResult | undefined> => {
|
|
1331
|
+
let ingestResult: IngestTurnResult | undefined;
|
|
1332
|
+
try {
|
|
1333
|
+
await dispatchOpenclawClawlingInbound({
|
|
1334
|
+
envelope: env as Envelope<unknown>,
|
|
1335
|
+
cfg,
|
|
1336
|
+
runtime,
|
|
1337
|
+
account,
|
|
1338
|
+
log,
|
|
1339
|
+
ingest: async (turn) => {
|
|
1340
|
+
ingestResult = await ingestTurn(turn);
|
|
573
1341
|
},
|
|
574
1342
|
});
|
|
575
1343
|
} catch (err) {
|
|
576
1344
|
log?.error?.(
|
|
577
1345
|
`[${accountId}] openclaw-clawchat message handler error: ${err instanceof Error ? err.stack || err.message : String(err)}`,
|
|
578
1346
|
);
|
|
1347
|
+
return "failed";
|
|
1348
|
+
}
|
|
1349
|
+
return ingestResult;
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1352
|
+
dispatchActivationBootstrap = async (): Promise<void> => {
|
|
1353
|
+
if (!store?.claimPendingActivationBootstrap || !store.markActivationBootstrapSent) return;
|
|
1354
|
+
let bootstrap: { conversationId: string } | null | undefined;
|
|
1355
|
+
const releaseBootstrap = () => {
|
|
1356
|
+
if (!bootstrap || !store.releaseActivationBootstrapClaim) return;
|
|
1357
|
+
const claimedBootstrap = bootstrap;
|
|
1358
|
+
recordConnection("activation bootstrap release", () =>
|
|
1359
|
+
store.releaseActivationBootstrapClaim?.({
|
|
1360
|
+
platform: "openclaw",
|
|
1361
|
+
accountId,
|
|
1362
|
+
conversationId: claimedBootstrap.conversationId,
|
|
1363
|
+
}),
|
|
1364
|
+
);
|
|
1365
|
+
};
|
|
1366
|
+
try {
|
|
1367
|
+
bootstrap = recordConnection("activation bootstrap claim", () =>
|
|
1368
|
+
store.claimPendingActivationBootstrap?.({ platform: "openclaw", accountId }),
|
|
1369
|
+
);
|
|
1370
|
+
if (!bootstrap) return;
|
|
1371
|
+
const claimedBootstrap = bootstrap;
|
|
1372
|
+
const result = await handleInboundEnvelope(
|
|
1373
|
+
buildActivationBootstrapEnvelope({ account, conversationId: claimedBootstrap.conversationId }),
|
|
1374
|
+
);
|
|
1375
|
+
if (result !== "submitted") {
|
|
1376
|
+
releaseBootstrap();
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
recordConnection("activation bootstrap sent", () =>
|
|
1380
|
+
store.markActivationBootstrapSent?.({
|
|
1381
|
+
platform: "openclaw",
|
|
1382
|
+
accountId,
|
|
1383
|
+
conversationId: claimedBootstrap.conversationId,
|
|
1384
|
+
}),
|
|
1385
|
+
);
|
|
1386
|
+
} catch (err) {
|
|
1387
|
+
releaseBootstrap();
|
|
1388
|
+
log?.error?.(
|
|
1389
|
+
`[${accountId}] openclaw-clawchat activation bootstrap failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1390
|
+
);
|
|
579
1391
|
}
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1394
|
+
client.on("message", (env: Envelope) => {
|
|
1395
|
+
void (async () => {
|
|
1396
|
+
await handleInboundEnvelope(env);
|
|
1397
|
+
})();
|
|
580
1398
|
});
|
|
581
1399
|
|
|
582
1400
|
// `client.connect()` resolves on `hello-ok` or rejects on `hello-fail`
|
|
583
1401
|
// (auth). Transport failures (server unreachable, DNS error, etc.) do
|
|
584
|
-
// NOT reject this promise — the
|
|
585
|
-
// its own exponential-backoff reconnect loop (`initialDelay * 2^attempt`
|
|
1402
|
+
// NOT reject this promise — the local client handles them internally and
|
|
1403
|
+
// drives its own exponential-backoff reconnect loop (`initialDelay * 2^attempt`
|
|
586
1404
|
// capped at `maxDelay`, with jitter). So we never throw here on anything
|
|
587
1405
|
// other than auth failure; on auth we tear the account down cleanly and
|
|
588
1406
|
// return without throwing (which would make the gateway supervisor
|
|
@@ -601,6 +1419,10 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
601
1419
|
lastError: classified.message,
|
|
602
1420
|
});
|
|
603
1421
|
if (classified.kind === "auth") {
|
|
1422
|
+
finishCurrentConnection({
|
|
1423
|
+
state: "auth_failed",
|
|
1424
|
+
error: lastHelloFailReason || classified.message,
|
|
1425
|
+
});
|
|
604
1426
|
logAuthFailure(classified.message);
|
|
605
1427
|
return;
|
|
606
1428
|
}
|
|
@@ -623,6 +1445,12 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
623
1445
|
log?.info?.(`[${accountId}] openclaw-clawchat runtime abort received; closing client`);
|
|
624
1446
|
activeClients.delete(accountId);
|
|
625
1447
|
closingForAbort = true;
|
|
1448
|
+
groupCoalescer.cancelAll();
|
|
1449
|
+
finishCurrentConnection({
|
|
1450
|
+
state: "disconnected",
|
|
1451
|
+
closeCode: 1000,
|
|
1452
|
+
closeReason: "client close",
|
|
1453
|
+
});
|
|
626
1454
|
client.close();
|
|
627
1455
|
setStatus({
|
|
628
1456
|
...getStatus(),
|