@clawling/clawchat-plugin-openclaw 2026.7.29-3 → 2026.8.4-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 +352 -51
- 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 +368 -45
- 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/dist/src/outbound.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomInt } from "node:crypto";
|
|
2
|
-
import { EVENT, MessageSendError } from "./protocol-types.js";
|
|
2
|
+
import { EVENT, MessageSendError, TERMINAL_CHAT_CODES } from "./protocol-types.js";
|
|
3
|
+
import { CHAT_ID_PREFIX, isValidChatId } from "./ws-client.js";
|
|
3
4
|
import { createAttachedChannelResultAdapter, } from "openclaw/plugin-sdk/channel-send-result";
|
|
4
5
|
import { chunkMarkdownText } from "openclaw/plugin-sdk/reply-runtime";
|
|
5
6
|
import { createOpenclawClawlingApiClient } from "./api-client.js";
|
|
@@ -115,14 +116,71 @@ function getAlignedMessageErrorTracker(client, account, log) {
|
|
|
115
116
|
export function setAlignedOutboundLogContext(client, context) {
|
|
116
117
|
alignedOutboundContexts.set(client, context);
|
|
117
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* The single outbound gate predicate. Narrow by design: only a SERVER-confirmed
|
|
121
|
+
* rejection blocks, never a `conversation.dissolved` inference, and never the
|
|
122
|
+
* owner's direct chat (the client applies that exemption inside isSendBlocked).
|
|
123
|
+
* Optional-call form because outbound tests hand-roll a client mock.
|
|
124
|
+
*/
|
|
125
|
+
function isOutboundBlocked(client, chatId) {
|
|
126
|
+
if (!chatId)
|
|
127
|
+
return false;
|
|
128
|
+
return client.isSendBlocked?.(chatId) === true;
|
|
129
|
+
}
|
|
118
130
|
export function flushAlignedOutboundQueue(client) {
|
|
119
131
|
const queue = alignedOutboundQueues.get(client);
|
|
120
|
-
|
|
132
|
+
// Re-check the gate at write time: this replay path (called on every
|
|
133
|
+
// `connected` edge) does not re-enter sendAlignedAckableEnvelope.
|
|
134
|
+
queue?.flush((wire) => client.sendWire(wire), { isBlocked: (item) => isOutboundBlocked(client, item.chatId) });
|
|
121
135
|
}
|
|
122
136
|
export function getAlignedOutboundQueueSize(client) {
|
|
123
137
|
return alignedOutboundQueues.get(client)?.snapshot().length ?? 0;
|
|
124
138
|
}
|
|
125
139
|
async function sendAlignedAckableEnvelope(params) {
|
|
140
|
+
// Static addressing check, before any server state is consulted: this one is
|
|
141
|
+
// decidable locally and is never revocable, so it is not a "gate" that can
|
|
142
|
+
// later clear. msghub refuses such a frame with `invalid conversation id`.
|
|
143
|
+
if (!isValidChatId(params.chatId)) {
|
|
144
|
+
params.log?.info?.(formatWsLog({
|
|
145
|
+
event: "send_blocked",
|
|
146
|
+
accountId: params.account.accountId,
|
|
147
|
+
...(alignedOutboundContexts.get(params.client)?.() ?? {
|
|
148
|
+
attempt: 1,
|
|
149
|
+
reconnectCount: 0,
|
|
150
|
+
state: params.client.transportState === "open" ? "ready" : "reconnecting",
|
|
151
|
+
}),
|
|
152
|
+
action: "drop",
|
|
153
|
+
fields: [
|
|
154
|
+
["event_name", params.eventName],
|
|
155
|
+
["chat_id", params.chatId],
|
|
156
|
+
["reason", "invalid_chat_id"],
|
|
157
|
+
],
|
|
158
|
+
}));
|
|
159
|
+
throw new MessageSendError("-", "invalid_chat_id", `"${params.chatId}" is not a ClawChat conversation id (expected ${CHAT_ID_PREFIX}…); refusing to send`, params.chatId);
|
|
160
|
+
}
|
|
161
|
+
// The server has already rejected this conversation with a terminal code and
|
|
162
|
+
// that rejection has not expired. Stop here: every further frame would be
|
|
163
|
+
// refused the same way. Gated on the narrow server-confirmed tier ONLY — a
|
|
164
|
+
// `conversation.dissolved` signal must never reach this branch, and the
|
|
165
|
+
// owner's direct chat is exempt (see ClawChatClient.isSendBlocked).
|
|
166
|
+
if (isOutboundBlocked(params.client, params.chatId)) {
|
|
167
|
+
params.log?.info?.(formatWsLog({
|
|
168
|
+
event: "send_blocked",
|
|
169
|
+
accountId: params.account.accountId,
|
|
170
|
+
...(alignedOutboundContexts.get(params.client)?.() ?? {
|
|
171
|
+
attempt: 1,
|
|
172
|
+
reconnectCount: 0,
|
|
173
|
+
state: params.client.transportState === "open" ? "ready" : "reconnecting",
|
|
174
|
+
}),
|
|
175
|
+
action: "drop",
|
|
176
|
+
fields: [
|
|
177
|
+
["event_name", params.eventName],
|
|
178
|
+
["chat_id", params.chatId],
|
|
179
|
+
["reason", "chat_rejected_by_server"],
|
|
180
|
+
],
|
|
181
|
+
}));
|
|
182
|
+
throw new MessageSendError("-", "chat_rejected_local", "conversation was rejected by the server; refusing to send", params.chatId);
|
|
183
|
+
}
|
|
126
184
|
const traceId = params.client.nextTraceId();
|
|
127
185
|
const env = {
|
|
128
186
|
version: "2",
|
|
@@ -206,6 +264,15 @@ async function sendAlignedAckableEnvelope(params) {
|
|
|
206
264
|
: typeof payload.message === "string" && payload.message
|
|
207
265
|
? payload.message
|
|
208
266
|
: "message send failed";
|
|
267
|
+
// A terminal refusal proves the conversation is unreachable for this
|
|
268
|
+
// sender right now. Record it on the SERVER tier so the gates below
|
|
269
|
+
// stop subsequent frames — this is fact, not the inference that
|
|
270
|
+
// `conversation.dissolved` gives us. It expires; see the client.
|
|
271
|
+
if (TERMINAL_CHAT_CODES.has(code)
|
|
272
|
+
&& typeof ack.chat_id === "string"
|
|
273
|
+
&& ack.chat_id) {
|
|
274
|
+
params.client.markChatDead?.(ack.chat_id, "server");
|
|
275
|
+
}
|
|
209
276
|
fail(new MessageSendError(traceId, code, hint, ack.chat_id));
|
|
210
277
|
return;
|
|
211
278
|
}
|
|
@@ -242,6 +309,9 @@ async function sendAlignedAckableEnvelope(params) {
|
|
|
242
309
|
onDrop: () => {
|
|
243
310
|
fail(new Error(`send queue full; dropped ${params.eventName} before write for trace_id=${traceId}`));
|
|
244
311
|
},
|
|
312
|
+
onBlock: () => {
|
|
313
|
+
fail(new MessageSendError("-", "chat_rejected_local", "conversation was rejected by the server while this frame was queued", params.chatId));
|
|
314
|
+
},
|
|
245
315
|
};
|
|
246
316
|
function isTerminalClose(close) {
|
|
247
317
|
return close?.code === 1000 || close?.reason === "client close" || close?.reason === "auth failed";
|
|
@@ -280,7 +350,7 @@ async function sendAlignedAckableEnvelope(params) {
|
|
|
280
350
|
}
|
|
281
351
|
try {
|
|
282
352
|
queue.enqueue(item);
|
|
283
|
-
queue.flush((queuedWire) => params.client.sendWire(queuedWire));
|
|
353
|
+
queue.flush((queuedWire) => params.client.sendWire(queuedWire), { isBlocked: (queued) => isOutboundBlocked(params.client, queued.chatId) });
|
|
284
354
|
}
|
|
285
355
|
catch {
|
|
286
356
|
// The queue keeps the failed frame at the head for reconnect retry, so
|
|
@@ -494,6 +564,14 @@ export async function sendOpenclawClawlingMedia(params) {
|
|
|
494
564
|
* path used for message.send/reply does not apply). Matches the mobile client.
|
|
495
565
|
*/
|
|
496
566
|
export function sendOpenclawClawlingReaction(params) {
|
|
567
|
+
if (!isValidChatId(params.chatId)) {
|
|
568
|
+
params.log?.info?.(`clawchat reaction blocked chat_id=${params.chatId} reason=invalid_chat_id`);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (isOutboundBlocked(params.client, params.chatId)) {
|
|
572
|
+
params.log?.info?.(`clawchat reaction blocked chat_id=${params.chatId} reason=chat_rejected_by_server`);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
497
575
|
const payload = {
|
|
498
576
|
target_message_id: params.targetMessageId,
|
|
499
577
|
emoji: params.emoji,
|
|
@@ -58,6 +58,24 @@ export class MessageSendError extends Error {
|
|
|
58
58
|
this.chatId = chatId;
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* `message.error` codes that PROVE the target conversation is unreachable for
|
|
63
|
+
* this sender right now (docs/client-integration.md §14.3). Both are documented
|
|
64
|
+
* as terminal for the affected uplink.
|
|
65
|
+
*
|
|
66
|
+
* Deliberately excludes `capability_missing` and `unsupported_version`: those
|
|
67
|
+
* are terminal too, but they indict THIS CLIENT's handshake/envelope, not the
|
|
68
|
+
* conversation — muting the chat on them would be a false positive.
|
|
69
|
+
*
|
|
70
|
+
* NOTE: "terminal for this uplink" is NOT "terminal forever". A deleted
|
|
71
|
+
* conversation can be revived in place under the same `cnv_` id (that is the
|
|
72
|
+
* path an agent re-pairing takes), so anything derived from these codes must
|
|
73
|
+
* be recoverable — see SERVER_REJECTION_TTL_MS in ws-client.ts.
|
|
74
|
+
*/
|
|
75
|
+
export const TERMINAL_CHAT_CODES = new Set([
|
|
76
|
+
"chat_not_found",
|
|
77
|
+
"not_member",
|
|
78
|
+
]);
|
|
61
79
|
export class StateError extends Error {
|
|
62
80
|
name = "StateError";
|
|
63
81
|
}
|
package/dist/src/runtime.js
CHANGED
|
@@ -8,7 +8,7 @@ import { buildActivationBootstrapText } from "./activation-greeting.js";
|
|
|
8
8
|
import { reportPluginVersionSafe, resolvePluginVersion } from "./plugin-report.js";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { ensureLivewareCli, resolveLivewarePath } from "./liveware-cli.js";
|
|
11
|
-
import { LivewareSampleSupervisor } from "./liveware-sample.js";
|
|
11
|
+
import { LivewareSampleSupervisor, } from "./liveware-sample.js";
|
|
12
12
|
import { ClawlingApiError } from "./api-types.js";
|
|
13
13
|
import { RefreshManager } from "./refresh-manager.js";
|
|
14
14
|
import { runOpenclawClawlingLogin, } from "./login.runtime.js";
|
|
@@ -32,11 +32,20 @@ import { createGroupMessageCoalescer } from "./group-message-coalescer.js";
|
|
|
32
32
|
import { openclawLlmContextDebug } from "./llm-context-debug.js";
|
|
33
33
|
import { GroupSettingsCache } from "./group-settings.js";
|
|
34
34
|
import { PermissionCache } from "./permissions.js";
|
|
35
|
-
import { buildAwarenessNoteEnvelope } from "./awareness-note-turn.js";
|
|
35
|
+
import { buildAwarenessNoteEnvelope, buildMomentCommentNoteEnvelope } from "./awareness-note-turn.js";
|
|
36
36
|
import { handlePermissionResult } from "./permission-result.js";
|
|
37
37
|
const { setRuntime: setOpenclawClawlingRuntime, getRuntime: getOpenclawClawlingRuntime } = createPluginRuntimeStore("clawchat-plugin-openclaw runtime not initialized");
|
|
38
38
|
export { setOpenclawClawlingRuntime, getOpenclawClawlingRuntime };
|
|
39
39
|
const activeClients = new Map();
|
|
40
|
+
// Live liveware-sample supervisors, keyed by the gateway's abortSignal and then by
|
|
41
|
+
// `platform:accountId`. startOpenclawClawlingGateway re-enters ITSELF (token-refresh
|
|
42
|
+
// reconnect, transport backoff, post-auth-failed re-activation) carrying the same
|
|
43
|
+
// `...params` — so the same abortSignal — and a per-call `new LivewareSampleSupervisor`
|
|
44
|
+
// left the previous one running: both then raced the bootstrap "account has zero apps"
|
|
45
|
+
// gate and each minted its own liveware app, leaving a duplicate app tile that no
|
|
46
|
+
// SQLite row tracks (the single-row PK means the loser is orphaned forever). Keyed by
|
|
47
|
+
// signal so an entry cannot outlive the channel it belongs to.
|
|
48
|
+
const livewareSampleSupervisors = new WeakMap();
|
|
40
49
|
const CLAWCHAT_PLUGIN_SLASH_COMMANDS = new Set(["clawchat-activate", "clawchat-output"]);
|
|
41
50
|
const ACTIVATION_CREDENTIAL_POLL_INTERVAL_MS = 2_000;
|
|
42
51
|
// Activation-bootstrap retry on the live connection. The bootstrap greeting is
|
|
@@ -791,7 +800,7 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
791
800
|
let livewareSampleNotify = async () => false;
|
|
792
801
|
if (store) {
|
|
793
802
|
const stateDirForSample = runtime.state?.resolveStateDir?.();
|
|
794
|
-
const
|
|
803
|
+
const livewareSampleDeps = {
|
|
795
804
|
platform: "openclaw",
|
|
796
805
|
accountId,
|
|
797
806
|
enabled: account.livewareSample,
|
|
@@ -819,9 +828,29 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
819
828
|
debug: (m) => log?.debug?.(m),
|
|
820
829
|
error: (m) => log?.error?.(m),
|
|
821
830
|
},
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
|
|
831
|
+
};
|
|
832
|
+
const supervisorsForSignal = livewareSampleSupervisors.get(abortSignal) ?? new Map();
|
|
833
|
+
livewareSampleSupervisors.set(abortSignal, supervisorsForSignal);
|
|
834
|
+
const supervisorKey = `openclaw:${accountId}`;
|
|
835
|
+
const runningSupervisor = supervisorsForSignal.get(supervisorKey);
|
|
836
|
+
if (runningSupervisor) {
|
|
837
|
+
// A gateway re-entry (refresh/backoff reconnect), not a fresh channel start:
|
|
838
|
+
// hand the live supervisor this closure's token + REST client + notify path
|
|
839
|
+
// instead of constructing a second one that would register a duplicate app.
|
|
840
|
+
log?.debug?.(`[${accountId}] clawchat-plugin-openclaw liveware-sample supervisor already live; re-pointing it at the re-entered gateway`);
|
|
841
|
+
runningSupervisor.adoptDeps(livewareSampleDeps);
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
const livewareSampleSupervisor = new LivewareSampleSupervisor(livewareSampleDeps);
|
|
845
|
+
supervisorsForSignal.set(supervisorKey, livewareSampleSupervisor);
|
|
846
|
+
abortSignal.addEventListener("abort", () => {
|
|
847
|
+
livewareSampleSupervisor.stop();
|
|
848
|
+
if (supervisorsForSignal.get(supervisorKey) === livewareSampleSupervisor) {
|
|
849
|
+
supervisorsForSignal.delete(supervisorKey);
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
void livewareSampleSupervisor.start();
|
|
853
|
+
}
|
|
825
854
|
}
|
|
826
855
|
const resolveMutateConfigFile = () => {
|
|
827
856
|
if (params.mutateConfigFile)
|
|
@@ -1840,6 +1869,30 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
1840
1869
|
],
|
|
1841
1870
|
}));
|
|
1842
1871
|
}
|
|
1872
|
+
else if (env.event === "message.error") {
|
|
1873
|
+
// Never fold this into `inbound_ignored`: `code` + `chat_id` are the
|
|
1874
|
+
// only in-plugin evidence that an uplink was refused, and the
|
|
1875
|
+
// 2026-07-19 incident needed a server-log dig purely because this line
|
|
1876
|
+
// did not exist. `action=message_error` marks it as semantically seen.
|
|
1877
|
+
const current = wsLogContext();
|
|
1878
|
+
const errorPayload = env.payload && typeof env.payload === "object"
|
|
1879
|
+
? env.payload
|
|
1880
|
+
: undefined;
|
|
1881
|
+
log?.info?.(formatWsLog({
|
|
1882
|
+
event: "inbound_control",
|
|
1883
|
+
accountId,
|
|
1884
|
+
attempt: current.attempt,
|
|
1885
|
+
reconnectCount: current.reconnectCount,
|
|
1886
|
+
state: "ready",
|
|
1887
|
+
action: "message_error",
|
|
1888
|
+
fields: [
|
|
1889
|
+
["event_name", env.event],
|
|
1890
|
+
["trace_id", env.trace_id],
|
|
1891
|
+
["chat_id", env.chat_id],
|
|
1892
|
+
["code", typeof errorPayload?.code === "string" ? errorPayload.code : "-"],
|
|
1893
|
+
],
|
|
1894
|
+
}));
|
|
1895
|
+
}
|
|
1843
1896
|
else if (env.event !== "ping" && env.event !== "pong") {
|
|
1844
1897
|
const current = wsLogContext();
|
|
1845
1898
|
log?.info?.(formatWsLog({
|
|
@@ -1909,6 +1962,12 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
1909
1962
|
?.conversationId?.trim();
|
|
1910
1963
|
return conversationId || null;
|
|
1911
1964
|
};
|
|
1965
|
+
// R3: the owner's direct conversation is never gated by the dead-chat send
|
|
1966
|
+
// guard. If it were, the very message explaining why the agent went dark
|
|
1967
|
+
// (skill-update notices, auth/logout notices, group owner-attention
|
|
1968
|
+
// forwards) would be unreachable. Registered lazily — the activation
|
|
1969
|
+
// conversation may be recorded after the client is constructed.
|
|
1970
|
+
client.setOwnerDirectChatResolver(resolveOwnerDirectConversationId);
|
|
1912
1971
|
const sendOwnerSkillMessage = async (text) => {
|
|
1913
1972
|
const conversationId = resolveOwnerDirectConversationId();
|
|
1914
1973
|
if (!conversationId) {
|
|
@@ -1996,7 +2055,11 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
1996
2055
|
// The conversation is gone. Evict it so no further uplink is emitted
|
|
1997
2056
|
// for it. Falls through to the awareness branch so the owner is told.
|
|
1998
2057
|
const entityId = typeof payload?.entity_id === "string" ? payload.entity_id : "";
|
|
1999
|
-
|
|
2058
|
+
// "signal" tier: this gates typing only. The protocol forbids gating
|
|
2059
|
+
// message.send on a dissolved signal — only a server rejection
|
|
2060
|
+
// (message.error, see src/outbound.ts) may do that, and unlike this
|
|
2061
|
+
// one that rejection expires.
|
|
2062
|
+
const evicted = client.markChatDead(entityId, "signal");
|
|
2000
2063
|
log?.info?.(evicted
|
|
2001
2064
|
? `[${accountId}] clawchat-plugin-openclaw conversation.dissolved signal received; evicting ${entityId}`
|
|
2002
2065
|
: `[${accountId}] clawchat-plugin-openclaw conversation.dissolved signal received with missing/invalid entity_id; no eviction performed`);
|
|
@@ -2018,6 +2081,32 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
2018
2081
|
log?.error?.(`[${accountId}] clawchat-plugin-openclaw skill-update check failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2019
2082
|
});
|
|
2020
2083
|
}
|
|
2084
|
+
else if (type === "moment.comment.created" || type === "moment.comment.replied") {
|
|
2085
|
+
// Content-free trigger: the note carries only the scenario + moment id
|
|
2086
|
+
// and points the agent at the get_moment tool — never the comment
|
|
2087
|
+
// text. Not gated behind account.awarenessNote and not debounced:
|
|
2088
|
+
// each moment-comment signal yields its own note (msghub already
|
|
2089
|
+
// coalesces multiple comments on one moment by entity_id).
|
|
2090
|
+
const momentId = typeof payload?.entity_id === "string" ? payload.entity_id : "";
|
|
2091
|
+
// No activation conversation ⇒ no routable chat_id. Skip the note
|
|
2092
|
+
// rather than address it to the owner's usr_… id, which
|
|
2093
|
+
// member-backend rejects as "invalid conversation id" (the agent's
|
|
2094
|
+
// in-turn replies are then dropped with no negative ack). Resolve
|
|
2095
|
+
// this FIRST, mirroring the friend.*/conversation.* awareness
|
|
2096
|
+
// branch's guard below.
|
|
2097
|
+
const ownerConversationId = resolveOwnerDirectConversationId();
|
|
2098
|
+
if (!ownerConversationId) {
|
|
2099
|
+
log?.info?.(`[${accountId}] clawchat-plugin-openclaw ${type} signal received but no owner conversation resolved; skipping moment awareness note (moment ${momentId})`);
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
log?.info?.(`[${accountId}] clawchat-plugin-openclaw ${type} signal received; synthesizing moment awareness note (moment ${momentId})`);
|
|
2103
|
+
void handleInboundEnvelope(buildMomentCommentNoteEnvelope({
|
|
2104
|
+
account,
|
|
2105
|
+
ownerConversationId,
|
|
2106
|
+
momentId,
|
|
2107
|
+
replied: type === "moment.comment.replied",
|
|
2108
|
+
}));
|
|
2109
|
+
}
|
|
2021
2110
|
else if (type === "friend.added" ||
|
|
2022
2111
|
type === "friend.removed" ||
|
|
2023
2112
|
type === "friend.profile_updated" ||
|
package/dist/src/tools-schema.js
CHANGED
|
@@ -198,6 +198,9 @@ export const ClawchatDeleteMomentSchema = Type.Object({
|
|
|
198
198
|
description: "Concrete ClawChat moment id to delete",
|
|
199
199
|
}),
|
|
200
200
|
});
|
|
201
|
+
export const ClawchatGetMomentSchema = Type.Object({
|
|
202
|
+
momentId: Type.Integer({ minimum: 1, description: "Concrete ClawChat moment id to fetch (with its visible comments)" }),
|
|
203
|
+
});
|
|
201
204
|
export const ClawchatToggleMomentReactionSchema = Type.Object({
|
|
202
205
|
momentId: Type.Integer({
|
|
203
206
|
minimum: 1,
|
package/dist/src/tools.js
CHANGED
|
@@ -12,7 +12,7 @@ import { getOpenclawClawlingClient, } from "./runtime.js";
|
|
|
12
12
|
import { markTerminalClawChatSend, getCurrentTerminalSendScope } from "./terminal-send.js";
|
|
13
13
|
import { editClawChatMemoryBody, readClawChatMemoryFile, resolveClawChatMemoryPath, searchClawChatMemory, writeClawChatMemoryBody, } from "./clawchat-memory.js";
|
|
14
14
|
import { pullGroupMetadata, pullOwnerMetadata, pullUserMetadata, pushMetadata, updateMetadata, } from "./clawchat-metadata.js";
|
|
15
|
-
import { ClawchatGetAccountProfileSchema, ClawchatGetConversationSchema, ClawchatGetUserProfileSchema, ClawchatLeaveGroupSchema, ClawchatAcceptFriendRequestSchema, ClawchatMemoryEditSchema, ClawchatMemoryReadSchema, ClawchatMemorySearchSchema, ClawchatMemoryWriteSchema, ClawchatMetadataSyncSchema, ClawchatMetadataUpdateSchema, ClawchatCreateMomentCommentSchema, ClawchatCreateMomentSchema, ClawchatDeleteMomentCommentSchema, ClawchatDeleteMomentSchema, ClawchatListAccountFriendsSchema, ClawchatListFriendRequestsSchema, ClawchatListMomentsSchema, ClawchatMentionMessageSchema, ClawchatReactMessageSchema, ClawchatReplyMomentCommentSchema, ClawchatRejectFriendRequestSchema, ClawchatRemoveFriendSchema, ClawchatSearchUsersSchema, ClawchatSendFriendRequestSchema, ClawchatToggleMomentReactionSchema, ClawchatUpdateAccountProfileSchema, ClawchatUploadAvatarImageSchema, ClawchatRegisterAppSchema, ClawchatListAppsSchema, ClawchatUnregisterAppSchema, ClawchatLivewareLoginSchema, } from "./tools-schema.js";
|
|
15
|
+
import { ClawchatGetAccountProfileSchema, ClawchatGetConversationSchema, ClawchatGetMomentSchema, ClawchatGetUserProfileSchema, ClawchatLeaveGroupSchema, ClawchatAcceptFriendRequestSchema, ClawchatMemoryEditSchema, ClawchatMemoryReadSchema, ClawchatMemorySearchSchema, ClawchatMemoryWriteSchema, ClawchatMetadataSyncSchema, ClawchatMetadataUpdateSchema, ClawchatCreateMomentCommentSchema, ClawchatCreateMomentSchema, ClawchatDeleteMomentCommentSchema, ClawchatDeleteMomentSchema, ClawchatListAccountFriendsSchema, ClawchatListFriendRequestsSchema, ClawchatListMomentsSchema, ClawchatMentionMessageSchema, ClawchatReactMessageSchema, ClawchatReplyMomentCommentSchema, ClawchatRejectFriendRequestSchema, ClawchatRemoveFriendSchema, ClawchatSearchUsersSchema, ClawchatSendFriendRequestSchema, ClawchatToggleMomentReactionSchema, ClawchatUpdateAccountProfileSchema, ClawchatUploadAvatarImageSchema, ClawchatRegisterAppSchema, ClawchatListAppsSchema, ClawchatUnregisterAppSchema, ClawchatLivewareLoginSchema, } from "./tools-schema.js";
|
|
16
16
|
const MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
|
|
17
17
|
// Owner-approval gate business codes (must match the ClawChat backend's owner-approval codes).
|
|
18
18
|
const CODE_PENDING_APPROVAL = 21001;
|
|
@@ -935,6 +935,18 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
935
935
|
});
|
|
936
936
|
},
|
|
937
937
|
}, { name: "clawchat_list_moments" });
|
|
938
|
+
api.registerTool({
|
|
939
|
+
name: "clawchat_get_moment",
|
|
940
|
+
label: "Get ClawChat Moment",
|
|
941
|
+
description: toolDescription("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."),
|
|
942
|
+
parameters: ClawchatGetMomentSchema,
|
|
943
|
+
async execute(_callId, params) {
|
|
944
|
+
return await recordClawchatToolCall("clawchat_get_moment", params, async () => {
|
|
945
|
+
const p = (params ?? {});
|
|
946
|
+
return await withClient((c) => c.getMoment(p.momentId));
|
|
947
|
+
});
|
|
948
|
+
},
|
|
949
|
+
}, { name: "clawchat_get_moment" });
|
|
938
950
|
api.registerTool({
|
|
939
951
|
name: "clawchat_create_moment",
|
|
940
952
|
label: "Create ClawChat Moment",
|
|
@@ -1196,5 +1208,5 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1196
1208
|
});
|
|
1197
1209
|
},
|
|
1198
1210
|
}, { name: "clawchat_liveware_login" });
|
|
1199
|
-
api.logger.debug?.("clawchat-plugin-openclaw: registered
|
|
1211
|
+
api.logger.debug?.("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)");
|
|
1200
1212
|
}
|
package/dist/src/ws-alignment.js
CHANGED
|
@@ -40,9 +40,26 @@ export function createAlignedWsQueue(options) {
|
|
|
40
40
|
queue.push(item);
|
|
41
41
|
logFrame("send_queued", "queue", item, [["queue_size", queue.length]]);
|
|
42
42
|
},
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Drain the queue. `isBlocked` is consulted PER ITEM at write time, not at
|
|
45
|
+
* enqueue time: a conversation can be rejected by the server *after* a
|
|
46
|
+
* frame was queued, and this replay path never re-enters the send function
|
|
47
|
+
* that guards the live path. Without this hook the outbound gate is
|
|
48
|
+
* bypassable by any reconnect (and by any batch flush that happens to carry
|
|
49
|
+
* a live chat's frame).
|
|
50
|
+
*/
|
|
51
|
+
flush(write, options = {}) {
|
|
44
52
|
while (queue.length > 0) {
|
|
45
53
|
const item = queue[0];
|
|
54
|
+
if (options.isBlocked?.(item)) {
|
|
55
|
+
queue.shift();
|
|
56
|
+
logFrame("send_blocked", "drop", item, [
|
|
57
|
+
["reason", "chat_rejected_by_server"],
|
|
58
|
+
["remaining", queue.length],
|
|
59
|
+
]);
|
|
60
|
+
item.onBlock?.();
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
46
63
|
try {
|
|
47
64
|
write(item.wire);
|
|
48
65
|
queue.shift();
|