@clawling/clawchat-plugin-openclaw 2026.7.29-3 → 2026.8.2-1
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/src/api-client.js +3 -0
- package/dist/src/awareness-note-turn.js +26 -0
- package/dist/src/client.js +1 -0
- package/dist/src/liveware-sample.js +272 -41
- package/dist/src/outbound.js +81 -3
- package/dist/src/protocol-types.js +18 -0
- package/dist/src/runtime.js +96 -7
- package/dist/src/tools-schema.js +3 -0
- package/dist/src/tools.js +14 -2
- package/dist/src/ws-alignment.js +18 -1
- package/dist/src/ws-client.js +246 -34
- package/openclaw.plugin.json +1 -0
- package/package.json +1 -1
- package/skills/clawchat-core/SKILL.md +3 -2
- package/skills/manifest.json +6 -6
- package/src/api-client.ts +4 -0
- package/src/awareness-note-turn.ts +45 -0
- package/src/client.ts +3 -0
- package/src/liveware-sample.ts +287 -41
- package/src/outbound.ts +118 -4
- package/src/protocol-types.ts +19 -0
- package/src/runtime.ts +112 -7
- package/src/storage.ts +8 -2
- package/src/tools-schema.ts +5 -0
- package/src/tools.ts +21 -1
- package/src/ws-alignment.ts +28 -1
- package/src/ws-client.ts +267 -32
package/src/outbound.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomInt } from "node:crypto";
|
|
2
|
-
import { EVENT, MessageSendError, type Envelope, type Fragment, type MentionFragment, type MessageAckPayload, type MessageErrorPayload, type MessageReactionPayload } from "./protocol-types.ts";
|
|
3
|
-
import type
|
|
2
|
+
import { EVENT, MessageSendError, TERMINAL_CHAT_CODES, type Envelope, type Fragment, type MentionFragment, type MessageAckPayload, type MessageErrorPayload, type MessageReactionPayload } from "./protocol-types.ts";
|
|
3
|
+
import { CHAT_ID_PREFIX, isValidChatId, type ClawlingChatClient } from "./ws-client.ts";
|
|
4
4
|
import {
|
|
5
5
|
createAttachedChannelResultAdapter,
|
|
6
6
|
type ChannelOutboundAdapter,
|
|
@@ -219,9 +219,25 @@ export function setAlignedOutboundLogContext(
|
|
|
219
219
|
alignedOutboundContexts.set(client as object, context);
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
/**
|
|
223
|
+
* The single outbound gate predicate. Narrow by design: only a SERVER-confirmed
|
|
224
|
+
* rejection blocks, never a `conversation.dissolved` inference, and never the
|
|
225
|
+
* owner's direct chat (the client applies that exemption inside isSendBlocked).
|
|
226
|
+
* Optional-call form because outbound tests hand-roll a client mock.
|
|
227
|
+
*/
|
|
228
|
+
function isOutboundBlocked(client: ClawlingChatClient, chatId: string | undefined): boolean {
|
|
229
|
+
if (!chatId) return false;
|
|
230
|
+
return (client as { isSendBlocked?: (chatId: string) => boolean }).isSendBlocked?.(chatId) === true;
|
|
231
|
+
}
|
|
232
|
+
|
|
222
233
|
export function flushAlignedOutboundQueue(client: ClawlingChatClient): void {
|
|
223
234
|
const queue = alignedOutboundQueues.get(client as object);
|
|
224
|
-
|
|
235
|
+
// Re-check the gate at write time: this replay path (called on every
|
|
236
|
+
// `connected` edge) does not re-enter sendAlignedAckableEnvelope.
|
|
237
|
+
queue?.flush(
|
|
238
|
+
(wire) => client.sendWire(wire),
|
|
239
|
+
{ isBlocked: (item) => isOutboundBlocked(client, item.chatId) },
|
|
240
|
+
);
|
|
225
241
|
}
|
|
226
242
|
|
|
227
243
|
export function getAlignedOutboundQueueSize(client: ClawlingChatClient): number {
|
|
@@ -236,6 +252,68 @@ async function sendAlignedAckableEnvelope(params: {
|
|
|
236
252
|
payload: object;
|
|
237
253
|
log?: LogSink;
|
|
238
254
|
}): Promise<Envelope<MessageAckPayload>> {
|
|
255
|
+
// Static addressing check, before any server state is consulted: this one is
|
|
256
|
+
// decidable locally and is never revocable, so it is not a "gate" that can
|
|
257
|
+
// later clear. msghub refuses such a frame with `invalid conversation id`.
|
|
258
|
+
if (!isValidChatId(params.chatId)) {
|
|
259
|
+
params.log?.info?.(
|
|
260
|
+
formatWsLog({
|
|
261
|
+
event: "send_blocked",
|
|
262
|
+
accountId: params.account.accountId,
|
|
263
|
+
...(
|
|
264
|
+
alignedOutboundContexts.get(params.client as object)?.() ?? {
|
|
265
|
+
attempt: 1,
|
|
266
|
+
reconnectCount: 0,
|
|
267
|
+
state: params.client.transportState === "open" ? "ready" : "reconnecting",
|
|
268
|
+
}
|
|
269
|
+
),
|
|
270
|
+
action: "drop",
|
|
271
|
+
fields: [
|
|
272
|
+
["event_name", params.eventName],
|
|
273
|
+
["chat_id", params.chatId],
|
|
274
|
+
["reason", "invalid_chat_id"],
|
|
275
|
+
],
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
throw new MessageSendError(
|
|
279
|
+
"-",
|
|
280
|
+
"invalid_chat_id",
|
|
281
|
+
`"${params.chatId}" is not a ClawChat conversation id (expected ${CHAT_ID_PREFIX}…); refusing to send`,
|
|
282
|
+
params.chatId,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
// The server has already rejected this conversation with a terminal code and
|
|
286
|
+
// that rejection has not expired. Stop here: every further frame would be
|
|
287
|
+
// refused the same way. Gated on the narrow server-confirmed tier ONLY — a
|
|
288
|
+
// `conversation.dissolved` signal must never reach this branch, and the
|
|
289
|
+
// owner's direct chat is exempt (see ClawChatClient.isSendBlocked).
|
|
290
|
+
if (isOutboundBlocked(params.client, params.chatId)) {
|
|
291
|
+
params.log?.info?.(
|
|
292
|
+
formatWsLog({
|
|
293
|
+
event: "send_blocked",
|
|
294
|
+
accountId: params.account.accountId,
|
|
295
|
+
...(
|
|
296
|
+
alignedOutboundContexts.get(params.client as object)?.() ?? {
|
|
297
|
+
attempt: 1,
|
|
298
|
+
reconnectCount: 0,
|
|
299
|
+
state: params.client.transportState === "open" ? "ready" : "reconnecting",
|
|
300
|
+
}
|
|
301
|
+
),
|
|
302
|
+
action: "drop",
|
|
303
|
+
fields: [
|
|
304
|
+
["event_name", params.eventName],
|
|
305
|
+
["chat_id", params.chatId],
|
|
306
|
+
["reason", "chat_rejected_by_server"],
|
|
307
|
+
],
|
|
308
|
+
}),
|
|
309
|
+
);
|
|
310
|
+
throw new MessageSendError(
|
|
311
|
+
"-",
|
|
312
|
+
"chat_rejected_local",
|
|
313
|
+
"conversation was rejected by the server; refusing to send",
|
|
314
|
+
params.chatId,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
239
317
|
const traceId = params.client.nextTraceId();
|
|
240
318
|
const env = {
|
|
241
319
|
version: "2" as const,
|
|
@@ -331,6 +409,19 @@ async function sendAlignedAckableEnvelope(params: {
|
|
|
331
409
|
: typeof payload.message === "string" && payload.message
|
|
332
410
|
? payload.message
|
|
333
411
|
: "message send failed";
|
|
412
|
+
// A terminal refusal proves the conversation is unreachable for this
|
|
413
|
+
// sender right now. Record it on the SERVER tier so the gates below
|
|
414
|
+
// stop subsequent frames — this is fact, not the inference that
|
|
415
|
+
// `conversation.dissolved` gives us. It expires; see the client.
|
|
416
|
+
if (
|
|
417
|
+
TERMINAL_CHAT_CODES.has(code)
|
|
418
|
+
&& typeof ack.chat_id === "string"
|
|
419
|
+
&& ack.chat_id
|
|
420
|
+
) {
|
|
421
|
+
(params.client as {
|
|
422
|
+
markChatDead?: (chatId: string, source: "signal" | "server") => boolean;
|
|
423
|
+
}).markChatDead?.(ack.chat_id, "server");
|
|
424
|
+
}
|
|
334
425
|
fail(new MessageSendError(traceId, code, hint, ack.chat_id));
|
|
335
426
|
return;
|
|
336
427
|
}
|
|
@@ -366,6 +457,14 @@ async function sendAlignedAckableEnvelope(params: {
|
|
|
366
457
|
onDrop: () => {
|
|
367
458
|
fail(new Error(`send queue full; dropped ${params.eventName} before write for trace_id=${traceId}`));
|
|
368
459
|
},
|
|
460
|
+
onBlock: () => {
|
|
461
|
+
fail(new MessageSendError(
|
|
462
|
+
"-",
|
|
463
|
+
"chat_rejected_local",
|
|
464
|
+
"conversation was rejected by the server while this frame was queued",
|
|
465
|
+
params.chatId,
|
|
466
|
+
));
|
|
467
|
+
},
|
|
369
468
|
};
|
|
370
469
|
|
|
371
470
|
function isTerminalClose(close: AlignedOutboundClose): boolean {
|
|
@@ -407,7 +506,10 @@ async function sendAlignedAckableEnvelope(params: {
|
|
|
407
506
|
|
|
408
507
|
try {
|
|
409
508
|
queue.enqueue(item);
|
|
410
|
-
queue.flush(
|
|
509
|
+
queue.flush(
|
|
510
|
+
(queuedWire) => params.client.sendWire(queuedWire),
|
|
511
|
+
{ isBlocked: (queued) => isOutboundBlocked(params.client, queued.chatId) },
|
|
512
|
+
);
|
|
411
513
|
} catch {
|
|
412
514
|
// The queue keeps the failed frame at the head for reconnect retry, so
|
|
413
515
|
// keep this promise pending until the frame is written+acked, dropped, or timed out.
|
|
@@ -663,6 +765,18 @@ export interface SendReactionParams {
|
|
|
663
765
|
* path used for message.send/reply does not apply). Matches the mobile client.
|
|
664
766
|
*/
|
|
665
767
|
export function sendOpenclawClawlingReaction(params: SendReactionParams): void {
|
|
768
|
+
if (!isValidChatId(params.chatId)) {
|
|
769
|
+
params.log?.info?.(
|
|
770
|
+
`clawchat reaction blocked chat_id=${params.chatId} reason=invalid_chat_id`,
|
|
771
|
+
);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
if (isOutboundBlocked(params.client, params.chatId)) {
|
|
775
|
+
params.log?.info?.(
|
|
776
|
+
`clawchat reaction blocked chat_id=${params.chatId} reason=chat_rejected_by_server`,
|
|
777
|
+
);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
666
780
|
const payload: MessageReactionPayload = {
|
|
667
781
|
target_message_id: params.targetMessageId,
|
|
668
782
|
emoji: params.emoji,
|
package/src/protocol-types.ts
CHANGED
|
@@ -326,6 +326,25 @@ export class MessageSendError extends Error {
|
|
|
326
326
|
}
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
+
/**
|
|
330
|
+
* `message.error` codes that PROVE the target conversation is unreachable for
|
|
331
|
+
* this sender right now (docs/client-integration.md §14.3). Both are documented
|
|
332
|
+
* as terminal for the affected uplink.
|
|
333
|
+
*
|
|
334
|
+
* Deliberately excludes `capability_missing` and `unsupported_version`: those
|
|
335
|
+
* are terminal too, but they indict THIS CLIENT's handshake/envelope, not the
|
|
336
|
+
* conversation — muting the chat on them would be a false positive.
|
|
337
|
+
*
|
|
338
|
+
* NOTE: "terminal for this uplink" is NOT "terminal forever". A deleted
|
|
339
|
+
* conversation can be revived in place under the same `cnv_` id (that is the
|
|
340
|
+
* path an agent re-pairing takes), so anything derived from these codes must
|
|
341
|
+
* be recoverable — see SERVER_REJECTION_TTL_MS in ws-client.ts.
|
|
342
|
+
*/
|
|
343
|
+
export const TERMINAL_CHAT_CODES: ReadonlySet<string> = new Set([
|
|
344
|
+
"chat_not_found",
|
|
345
|
+
"not_member",
|
|
346
|
+
]);
|
|
347
|
+
|
|
329
348
|
export class StateError extends Error {
|
|
330
349
|
override name = "StateError";
|
|
331
350
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -19,7 +19,11 @@ import { buildActivationBootstrapText } from "./activation-greeting.ts";
|
|
|
19
19
|
import { reportPluginVersionSafe, resolvePluginVersion } from "./plugin-report.ts";
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
import { ensureLivewareCli, resolveLivewarePath } from "./liveware-cli.ts";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
LivewareSampleSupervisor,
|
|
24
|
+
type LivewareSampleStoreLike,
|
|
25
|
+
type LivewareSampleSupervisorDeps,
|
|
26
|
+
} from "./liveware-sample.ts";
|
|
23
27
|
import { ClawlingApiError } from "./api-types.ts";
|
|
24
28
|
import { RefreshManager } from "./refresh-manager.ts";
|
|
25
29
|
import {
|
|
@@ -85,7 +89,7 @@ import { createGroupMessageCoalescer } from "./group-message-coalescer.ts";
|
|
|
85
89
|
import { openclawLlmContextDebug } from "./llm-context-debug.ts";
|
|
86
90
|
import { GroupSettingsCache } from "./group-settings.ts";
|
|
87
91
|
import { PermissionCache } from "./permissions.ts";
|
|
88
|
-
import { buildAwarenessNoteEnvelope } from "./awareness-note-turn.ts";
|
|
92
|
+
import { buildAwarenessNoteEnvelope, buildMomentCommentNoteEnvelope } from "./awareness-note-turn.ts";
|
|
89
93
|
import { handlePermissionResult } from "./permission-result.ts";
|
|
90
94
|
|
|
91
95
|
type Log = { debug?: (m: string) => void; info?: (m: string) => void; error?: (m: string) => void };
|
|
@@ -129,6 +133,18 @@ const { setRuntime: setOpenclawClawlingRuntime, getRuntime: getOpenclawClawlingR
|
|
|
129
133
|
export { setOpenclawClawlingRuntime, getOpenclawClawlingRuntime };
|
|
130
134
|
|
|
131
135
|
const activeClients = new Map<string, ClawlingChatClient>();
|
|
136
|
+
// Live liveware-sample supervisors, keyed by the gateway's abortSignal and then by
|
|
137
|
+
// `platform:accountId`. startOpenclawClawlingGateway re-enters ITSELF (token-refresh
|
|
138
|
+
// reconnect, transport backoff, post-auth-failed re-activation) carrying the same
|
|
139
|
+
// `...params` — so the same abortSignal — and a per-call `new LivewareSampleSupervisor`
|
|
140
|
+
// left the previous one running: both then raced the bootstrap "account has zero apps"
|
|
141
|
+
// gate and each minted its own liveware app, leaving a duplicate app tile that no
|
|
142
|
+
// SQLite row tracks (the single-row PK means the loser is orphaned forever). Keyed by
|
|
143
|
+
// signal so an entry cannot outlive the channel it belongs to.
|
|
144
|
+
const livewareSampleSupervisors = new WeakMap<
|
|
145
|
+
AbortSignal,
|
|
146
|
+
Map<string, LivewareSampleSupervisor>
|
|
147
|
+
>();
|
|
132
148
|
const CLAWCHAT_PLUGIN_SLASH_COMMANDS = new Set(["clawchat-activate", "clawchat-output"]);
|
|
133
149
|
const ACTIVATION_CREDENTIAL_POLL_INTERVAL_MS = 2_000;
|
|
134
150
|
// Activation-bootstrap retry on the live connection. The bootstrap greeting is
|
|
@@ -1111,7 +1127,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1111
1127
|
let livewareSampleNotify: (text: string) => Promise<boolean> = async () => false;
|
|
1112
1128
|
if (store) {
|
|
1113
1129
|
const stateDirForSample = runtime.state?.resolveStateDir?.();
|
|
1114
|
-
const
|
|
1130
|
+
const livewareSampleDeps: LivewareSampleSupervisorDeps = {
|
|
1115
1131
|
platform: "openclaw",
|
|
1116
1132
|
accountId,
|
|
1117
1133
|
enabled: account.livewareSample,
|
|
@@ -1139,9 +1155,31 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1139
1155
|
debug: (m) => log?.debug?.(m),
|
|
1140
1156
|
error: (m) => log?.error?.(m),
|
|
1141
1157
|
},
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
|
|
1158
|
+
};
|
|
1159
|
+
const supervisorsForSignal =
|
|
1160
|
+
livewareSampleSupervisors.get(abortSignal) ?? new Map<string, LivewareSampleSupervisor>();
|
|
1161
|
+
livewareSampleSupervisors.set(abortSignal, supervisorsForSignal);
|
|
1162
|
+
const supervisorKey = `openclaw:${accountId}`;
|
|
1163
|
+
const runningSupervisor = supervisorsForSignal.get(supervisorKey);
|
|
1164
|
+
if (runningSupervisor) {
|
|
1165
|
+
// A gateway re-entry (refresh/backoff reconnect), not a fresh channel start:
|
|
1166
|
+
// hand the live supervisor this closure's token + REST client + notify path
|
|
1167
|
+
// instead of constructing a second one that would register a duplicate app.
|
|
1168
|
+
log?.debug?.(
|
|
1169
|
+
`[${accountId}] clawchat-plugin-openclaw liveware-sample supervisor already live; re-pointing it at the re-entered gateway`,
|
|
1170
|
+
);
|
|
1171
|
+
runningSupervisor.adoptDeps(livewareSampleDeps);
|
|
1172
|
+
} else {
|
|
1173
|
+
const livewareSampleSupervisor = new LivewareSampleSupervisor(livewareSampleDeps);
|
|
1174
|
+
supervisorsForSignal.set(supervisorKey, livewareSampleSupervisor);
|
|
1175
|
+
abortSignal.addEventListener("abort", () => {
|
|
1176
|
+
livewareSampleSupervisor.stop();
|
|
1177
|
+
if (supervisorsForSignal.get(supervisorKey) === livewareSampleSupervisor) {
|
|
1178
|
+
supervisorsForSignal.delete(supervisorKey);
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
void livewareSampleSupervisor.start();
|
|
1182
|
+
}
|
|
1145
1183
|
}
|
|
1146
1184
|
|
|
1147
1185
|
const resolveMutateConfigFile = (): OpenclawClawchatMutateConfigFile | undefined => {
|
|
@@ -2236,6 +2274,31 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2236
2274
|
],
|
|
2237
2275
|
}),
|
|
2238
2276
|
);
|
|
2277
|
+
} else if (env.event === "message.error") {
|
|
2278
|
+
// Never fold this into `inbound_ignored`: `code` + `chat_id` are the
|
|
2279
|
+
// only in-plugin evidence that an uplink was refused, and the
|
|
2280
|
+
// 2026-07-19 incident needed a server-log dig purely because this line
|
|
2281
|
+
// did not exist. `action=message_error` marks it as semantically seen.
|
|
2282
|
+
const current = wsLogContext();
|
|
2283
|
+
const errorPayload = env.payload && typeof env.payload === "object"
|
|
2284
|
+
? (env.payload as { code?: unknown })
|
|
2285
|
+
: undefined;
|
|
2286
|
+
log?.info?.(
|
|
2287
|
+
formatWsLog({
|
|
2288
|
+
event: "inbound_control",
|
|
2289
|
+
accountId,
|
|
2290
|
+
attempt: current.attempt,
|
|
2291
|
+
reconnectCount: current.reconnectCount,
|
|
2292
|
+
state: "ready",
|
|
2293
|
+
action: "message_error",
|
|
2294
|
+
fields: [
|
|
2295
|
+
["event_name", env.event],
|
|
2296
|
+
["trace_id", env.trace_id],
|
|
2297
|
+
["chat_id", env.chat_id],
|
|
2298
|
+
["code", typeof errorPayload?.code === "string" ? errorPayload.code : "-"],
|
|
2299
|
+
],
|
|
2300
|
+
}),
|
|
2301
|
+
);
|
|
2239
2302
|
} else if (env.event !== "ping" && env.event !== "pong") {
|
|
2240
2303
|
const current = wsLogContext();
|
|
2241
2304
|
log?.info?.(
|
|
@@ -2310,6 +2373,12 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2310
2373
|
?.conversationId?.trim();
|
|
2311
2374
|
return conversationId || null;
|
|
2312
2375
|
};
|
|
2376
|
+
// R3: the owner's direct conversation is never gated by the dead-chat send
|
|
2377
|
+
// guard. If it were, the very message explaining why the agent went dark
|
|
2378
|
+
// (skill-update notices, auth/logout notices, group owner-attention
|
|
2379
|
+
// forwards) would be unreachable. Registered lazily — the activation
|
|
2380
|
+
// conversation may be recorded after the client is constructed.
|
|
2381
|
+
client.setOwnerDirectChatResolver(resolveOwnerDirectConversationId);
|
|
2313
2382
|
const sendOwnerSkillMessage = async (text: string): Promise<void> => {
|
|
2314
2383
|
const conversationId = resolveOwnerDirectConversationId();
|
|
2315
2384
|
if (!conversationId) {
|
|
@@ -2406,7 +2475,11 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2406
2475
|
// for it. Falls through to the awareness branch so the owner is told.
|
|
2407
2476
|
const entityId =
|
|
2408
2477
|
typeof payload?.entity_id === "string" ? payload.entity_id : "";
|
|
2409
|
-
|
|
2478
|
+
// "signal" tier: this gates typing only. The protocol forbids gating
|
|
2479
|
+
// message.send on a dissolved signal — only a server rejection
|
|
2480
|
+
// (message.error, see src/outbound.ts) may do that, and unlike this
|
|
2481
|
+
// one that rejection expires.
|
|
2482
|
+
const evicted = client.markChatDead(entityId, "signal");
|
|
2410
2483
|
log?.info?.(
|
|
2411
2484
|
evicted
|
|
2412
2485
|
? `[${accountId}] clawchat-plugin-openclaw conversation.dissolved signal received; evicting ${entityId}`
|
|
@@ -2437,6 +2510,38 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2437
2510
|
}`,
|
|
2438
2511
|
);
|
|
2439
2512
|
});
|
|
2513
|
+
} else if (type === "moment.comment.created" || type === "moment.comment.replied") {
|
|
2514
|
+
// Content-free trigger: the note carries only the scenario + moment id
|
|
2515
|
+
// and points the agent at the get_moment tool — never the comment
|
|
2516
|
+
// text. Not gated behind account.awarenessNote and not debounced:
|
|
2517
|
+
// each moment-comment signal yields its own note (msghub already
|
|
2518
|
+
// coalesces multiple comments on one moment by entity_id).
|
|
2519
|
+
const momentId =
|
|
2520
|
+
typeof payload?.entity_id === "string" ? payload.entity_id : "";
|
|
2521
|
+
// No activation conversation ⇒ no routable chat_id. Skip the note
|
|
2522
|
+
// rather than address it to the owner's usr_… id, which
|
|
2523
|
+
// member-backend rejects as "invalid conversation id" (the agent's
|
|
2524
|
+
// in-turn replies are then dropped with no negative ack). Resolve
|
|
2525
|
+
// this FIRST, mirroring the friend.*/conversation.* awareness
|
|
2526
|
+
// branch's guard below.
|
|
2527
|
+
const ownerConversationId = resolveOwnerDirectConversationId();
|
|
2528
|
+
if (!ownerConversationId) {
|
|
2529
|
+
log?.info?.(
|
|
2530
|
+
`[${accountId}] clawchat-plugin-openclaw ${type} signal received but no owner conversation resolved; skipping moment awareness note (moment ${momentId})`,
|
|
2531
|
+
);
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
log?.info?.(
|
|
2535
|
+
`[${accountId}] clawchat-plugin-openclaw ${type} signal received; synthesizing moment awareness note (moment ${momentId})`,
|
|
2536
|
+
);
|
|
2537
|
+
void handleInboundEnvelope(
|
|
2538
|
+
buildMomentCommentNoteEnvelope({
|
|
2539
|
+
account,
|
|
2540
|
+
ownerConversationId,
|
|
2541
|
+
momentId,
|
|
2542
|
+
replied: type === "moment.comment.replied",
|
|
2543
|
+
}),
|
|
2544
|
+
);
|
|
2440
2545
|
} else if (
|
|
2441
2546
|
type === "friend.added" ||
|
|
2442
2547
|
type === "friend.removed" ||
|
package/src/storage.ts
CHANGED
|
@@ -180,14 +180,20 @@ export type LivewareSampleUpsert = {
|
|
|
180
180
|
port: number;
|
|
181
181
|
publicUrl?: string | null;
|
|
182
182
|
sampleVersion: string;
|
|
183
|
-
|
|
183
|
+
// "pending": `liveware app create` succeeded but the app is not registered with
|
|
184
|
+
// ClawChat yet. Persisted mid-bootstrap so a failure after create resumes with
|
|
185
|
+
// the same app id instead of minting (and leaking) another one.
|
|
186
|
+
status: "pending" | "active" | "failed" | "disabled";
|
|
184
187
|
lastError?: string | null;
|
|
185
188
|
};
|
|
186
189
|
|
|
187
190
|
export type UpdateLivewareSampleStatusInput = {
|
|
188
191
|
platform: string;
|
|
189
192
|
accountId: string;
|
|
190
|
-
|
|
193
|
+
// "pending": `liveware app create` succeeded but the app is not registered with
|
|
194
|
+
// ClawChat yet. Persisted mid-bootstrap so a failure after create resumes with
|
|
195
|
+
// the same app id instead of minting (and leaking) another one.
|
|
196
|
+
status: "pending" | "active" | "failed" | "disabled";
|
|
191
197
|
lastError?: string | null;
|
|
192
198
|
};
|
|
193
199
|
|
package/src/tools-schema.ts
CHANGED
|
@@ -323,6 +323,11 @@ export const ClawchatDeleteMomentSchema = Type.Object({
|
|
|
323
323
|
});
|
|
324
324
|
export type ClawchatDeleteMomentParams = Static<typeof ClawchatDeleteMomentSchema>;
|
|
325
325
|
|
|
326
|
+
export const ClawchatGetMomentSchema = Type.Object({
|
|
327
|
+
momentId: Type.Integer({ minimum: 1, description: "Concrete ClawChat moment id to fetch (with its visible comments)" }),
|
|
328
|
+
});
|
|
329
|
+
export type ClawchatGetMomentParams = Static<typeof ClawchatGetMomentSchema>;
|
|
330
|
+
|
|
326
331
|
export const ClawchatToggleMomentReactionSchema = Type.Object({
|
|
327
332
|
momentId: Type.Integer({
|
|
328
333
|
minimum: 1,
|
package/src/tools.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
import {
|
|
41
41
|
ClawchatGetAccountProfileSchema,
|
|
42
42
|
ClawchatGetConversationSchema,
|
|
43
|
+
ClawchatGetMomentSchema,
|
|
43
44
|
ClawchatGetUserProfileSchema,
|
|
44
45
|
ClawchatLeaveGroupSchema,
|
|
45
46
|
ClawchatAcceptFriendRequestSchema,
|
|
@@ -82,6 +83,7 @@ import {
|
|
|
82
83
|
type ClawchatDeleteMomentParams,
|
|
83
84
|
type ClawchatAcceptFriendRequestParams,
|
|
84
85
|
type ClawchatGetConversationParams,
|
|
86
|
+
type ClawchatGetMomentParams,
|
|
85
87
|
type ClawchatGetUserProfileParams,
|
|
86
88
|
type ClawchatLeaveGroupParams,
|
|
87
89
|
type ClawchatListFriendRequestsParams,
|
|
@@ -1194,6 +1196,24 @@ export function registerOpenclawClawlingTools(
|
|
|
1194
1196
|
{ name: "clawchat_list_moments" },
|
|
1195
1197
|
);
|
|
1196
1198
|
|
|
1199
|
+
api.registerTool(
|
|
1200
|
+
{
|
|
1201
|
+
name: "clawchat_get_moment",
|
|
1202
|
+
label: "Get ClawChat Moment",
|
|
1203
|
+
description: toolDescription(
|
|
1204
|
+
"Fetch a single ClawChat moment by id together with the comments visible to this agent. TRIGGER - after a moment.comment.created / moment.comment.replied awareness note, call this with the moment id to read the new comment before deciding whether to reply.",
|
|
1205
|
+
),
|
|
1206
|
+
parameters: ClawchatGetMomentSchema,
|
|
1207
|
+
async execute(_callId, params) {
|
|
1208
|
+
return await recordClawchatToolCall("clawchat_get_moment", params, async () => {
|
|
1209
|
+
const p = (params ?? {}) as ClawchatGetMomentParams;
|
|
1210
|
+
return await withClient((c) => c.getMoment(p.momentId));
|
|
1211
|
+
});
|
|
1212
|
+
},
|
|
1213
|
+
},
|
|
1214
|
+
{ name: "clawchat_get_moment" },
|
|
1215
|
+
);
|
|
1216
|
+
|
|
1197
1217
|
api.registerTool(
|
|
1198
1218
|
{
|
|
1199
1219
|
name: "clawchat_create_moment",
|
|
@@ -1544,6 +1564,6 @@ export function registerOpenclawClawlingTools(
|
|
|
1544
1564
|
);
|
|
1545
1565
|
|
|
1546
1566
|
api.logger.debug?.(
|
|
1547
|
-
"clawchat-plugin-openclaw: registered
|
|
1567
|
+
"clawchat-plugin-openclaw: registered 31 clawchat_* tools (get_account_profile, get_user_profile, list_account_friends, send_friend_request, list_friend_requests, accept_friend_request, reject_friend_request, remove_friend, search_users, get_conversation, mention_message, list_moments, get_moment, create_moment, delete_moment, toggle_moment_reaction, create_moment_comment, reply_moment_comment, delete_moment_comment, update_account_profile, upload_avatar_image, memory_search, memory_read, memory_write, memory_edit, metadata_sync, metadata_update, register_app, list_apps, unregister_app, liveware_login)",
|
|
1548
1568
|
);
|
|
1549
1569
|
}
|
package/src/ws-alignment.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface AlignedWsQueueItem {
|
|
|
9
9
|
wire: string;
|
|
10
10
|
onWrite?: () => void;
|
|
11
11
|
onDrop?: () => void;
|
|
12
|
+
/** Called instead of `write` when the flush-time gate blocks this item. */
|
|
13
|
+
onBlock?: () => void;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export interface CreateAlignedWsQueueOptions {
|
|
@@ -85,9 +87,34 @@ export function createAlignedWsQueue(options: CreateAlignedWsQueueOptions) {
|
|
|
85
87
|
logFrame("send_queued", "queue", item, [["queue_size", queue.length]]);
|
|
86
88
|
},
|
|
87
89
|
|
|
88
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Drain the queue. `isBlocked` is consulted PER ITEM at write time, not at
|
|
92
|
+
* enqueue time: a conversation can be rejected by the server *after* a
|
|
93
|
+
* frame was queued, and this replay path never re-enters the send function
|
|
94
|
+
* that guards the live path. Without this hook the outbound gate is
|
|
95
|
+
* bypassable by any reconnect (and by any batch flush that happens to carry
|
|
96
|
+
* a live chat's frame).
|
|
97
|
+
*/
|
|
98
|
+
flush(
|
|
99
|
+
write: (wire: string) => void,
|
|
100
|
+
options: { isBlocked?: (item: AlignedWsQueueItem) => boolean } = {},
|
|
101
|
+
) {
|
|
89
102
|
while (queue.length > 0) {
|
|
90
103
|
const item = queue[0]!;
|
|
104
|
+
if (options.isBlocked?.(item)) {
|
|
105
|
+
queue.shift();
|
|
106
|
+
logFrame(
|
|
107
|
+
"send_blocked",
|
|
108
|
+
"drop",
|
|
109
|
+
item,
|
|
110
|
+
[
|
|
111
|
+
["reason", "chat_rejected_by_server"],
|
|
112
|
+
["remaining", queue.length],
|
|
113
|
+
],
|
|
114
|
+
);
|
|
115
|
+
item.onBlock?.();
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
91
118
|
try {
|
|
92
119
|
write(item.wire);
|
|
93
120
|
queue.shift();
|