@clawling/clawchat-plugin-openclaw 2026.7.29-1 → 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.
@@ -64,9 +64,16 @@ function buildOutcomeNote(metadata) {
64
64
  * Mirrors the `buildAwarenessNoteEnvelope` pattern: a synthetic inbound
65
65
  * `message.send` targeting the owner's direct conversation, routed through
66
66
  * `handleInboundEnvelope` so the agent can reason about the outcome.
67
+ *
68
+ * `ownerConversationId` MUST be the owner's direct conversation id (`cnv_…`)
69
+ * recorded at activation, never `account.ownerUserId`. The agent's in-turn
70
+ * reply inherits this note's `chat_id`, and msghub resolves chat_id through
71
+ * member-backend — a `usr_…` there comes back "invalid conversation id", so the
72
+ * reply is dropped and the sender never learns. Hence the required non-null
73
+ * parameter: there is no safe value to fall back to.
67
74
  */
68
75
  export function buildPermissionResultEnvelope(params) {
69
- const { account, metadata } = params;
76
+ const { account, metadata, ownerConversationId } = params;
70
77
  const text = buildOutcomeNote(metadata);
71
78
  const now = Date.now();
72
79
  return {
@@ -74,7 +81,7 @@ export function buildPermissionResultEnvelope(params) {
74
81
  event: EVENT.MESSAGE_SEND,
75
82
  trace_id: `clawchat-plugin-openclaw-permission-result-${metadata.request_id}-${now}`,
76
83
  emitted_at: now,
77
- chat_id: account.ownerUserId,
84
+ chat_id: ownerConversationId,
78
85
  chat_type: "direct",
79
86
  to: { id: account.userId, type: "direct" },
80
87
  sender: {
@@ -109,12 +116,20 @@ export function buildPermissionResultEnvelope(params) {
109
116
  *
110
117
  * Returns a synthetic context-note {@link Envelope} on a fresh receipt so
111
118
  * the caller can feed it to `handleInboundEnvelope`; returns `null` when
112
- * the `request_id` has already been processed.
119
+ * the `request_id` has already been processed, or when `ownerConversationId`
120
+ * is absent.
121
+ *
122
+ * A null `ownerConversationId` (activation has not recorded the owner's direct
123
+ * conversation yet) is checked BEFORE the dedup mark on purpose: the receipt
124
+ * cannot be routed anywhere, so burning its dedup slot would permanently
125
+ * swallow it — the replayed copy must still render once the id is known.
113
126
  */
114
- export function handlePermissionResult(metadata, account) {
127
+ export function handlePermissionResult(metadata, account, ownerConversationId) {
128
+ if (!ownerConversationId)
129
+ return null;
115
130
  if (metadata.request_id && _seenRequestIds.has(metadata.request_id))
116
131
  return null;
117
132
  if (metadata.request_id)
118
133
  _seenRequestIds.add(metadata.request_id);
119
- return buildPermissionResultEnvelope({ account, metadata });
134
+ return buildPermissionResultEnvelope({ account, metadata, ownerConversationId });
120
135
  }
@@ -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
  }
@@ -256,14 +256,23 @@ function formatApprovalSummary(payload) {
256
256
  * as separate ClawChat messages.
257
257
  */
258
258
  export function createOpenclawClawlingReplyDispatcher(options) {
259
- const { cfg, runtime, account, client, target, replyCtx, inboundMessageId, store, log, } = options;
259
+ const { cfg, runtime, account, client, target, replyCtx, inboundMessageId, store, resolveOwnerConversationId, log, } = options;
260
260
  const isGroupTarget = target.chatType === "group";
261
261
  const outputVisibility = effectiveOutputVisibility(account, target.chatId, target.chatType);
262
262
  const splitFullOutput = outputVisibility === "full";
263
263
  const splitNormalBlockOutput = outputVisibility === "normal";
264
+ // The escalation target is the owner's ACTIVATION CONVERSATION id, never the
265
+ // owner's usr_… id: chat_id is resolved by msghub through member-backend, and
266
+ // a usr_… comes back "invalid conversation id" — the frame is dropped with no
267
+ // negative ack to this client, so the owner silently never gets the approval
268
+ // request. Both ids are required: ownerUserId gates whether an owner exists at
269
+ // all, the conversation id is where a frame can actually be delivered.
264
270
  const ownerDirectTarget = () => {
265
271
  const ownerUserId = account.ownerUserId?.trim();
266
- return ownerUserId ? { chatId: ownerUserId, chatType: "direct" } : null;
272
+ if (!ownerUserId)
273
+ return null;
274
+ const conversationId = resolveOwnerConversationId?.()?.trim();
275
+ return conversationId ? { chatId: conversationId, chatType: "direct" } : null;
267
276
  };
268
277
  const humanDelay = runtime.channel.reply.resolveHumanDelayConfig(cfg, account.userId);
269
278
  const buildApiClient = () => {
@@ -448,7 +457,10 @@ export function createOpenclawClawlingReplyDispatcher(options) {
448
457
  const sendOwnerAttention = async (fallbackText, richFragment = null) => {
449
458
  const ownerTarget = ownerDirectTarget();
450
459
  if (!ownerTarget) {
451
- log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw group owner attention suppressed reason=missing_owner_user_id group=${target.chatId}`);
460
+ const reason = account.ownerUserId?.trim()
461
+ ? "missing_owner_conversation"
462
+ : "missing_owner_user_id";
463
+ log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw group owner attention suppressed reason=${reason} group=${target.chatId}`);
452
464
  return;
453
465
  }
454
466
  await sendOpenclawClawlingText({
@@ -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 livewareSampleSupervisor = new LivewareSampleSupervisor({
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
- abortSignal.addEventListener("abort", () => livewareSampleSupervisor.stop());
824
- void livewareSampleSupervisor.start();
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
- const evicted = client.markChatDead(entityId);
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" ||
@@ -2028,10 +2117,16 @@ export async function startOpenclawClawlingGateway(params) {
2028
2117
  pendingAwarenessNote = true;
2029
2118
  queueMicrotask(() => {
2030
2119
  pendingAwarenessNote = false;
2031
- void handleInboundEnvelope(buildAwarenessNoteEnvelope({
2032
- account,
2033
- ownerConversationId: resolveOwnerDirectConversationId(),
2034
- }));
2120
+ // No activation conversation ⇒ no routable chat_id. Skip the note
2121
+ // rather than address it to the owner's usr_… id, which
2122
+ // member-backend rejects as "invalid conversation id" (the agent's
2123
+ // in-turn replies are then dropped with no negative ack).
2124
+ const ownerConversationId = resolveOwnerDirectConversationId();
2125
+ if (!ownerConversationId) {
2126
+ log?.info?.(`[${accountId}] clawchat-plugin-openclaw awareness note skipped: no activation conversation recorded`);
2127
+ return;
2128
+ }
2129
+ void handleInboundEnvelope(buildAwarenessNoteEnvelope({ account, ownerConversationId }));
2035
2130
  });
2036
2131
  }
2037
2132
  }
@@ -2405,6 +2500,10 @@ export async function startOpenclawClawlingGateway(params) {
2405
2500
  updateMessageByIdentity: (input) => store.updateMessageByIdentity?.(input),
2406
2501
  }
2407
2502
  : null,
2503
+ // Group owner-attention escalations address the owner's activation
2504
+ // conversation, not `ownerUserId` (a usr_… chat_id is rejected by
2505
+ // member-backend). Passed as a thunk so it is resolved at send time.
2506
+ resolveOwnerConversationId: resolveOwnerDirectConversationId,
2408
2507
  log,
2409
2508
  });
2410
2509
  const agentsConfigured = Object.keys(cfg.agents ?? {});
@@ -2658,7 +2757,21 @@ export async function startOpenclawClawlingGateway(params) {
2658
2757
  // A permission_result receipt closes the loop on an earlier request.
2659
2758
  // Dedup by request_id; on a fresh receipt feed the synthetic context
2660
2759
  // note back to the agent so it can react to the outcome.
2661
- const note = handlePermissionResult(metadata, account);
2760
+ //
2761
+ // The note is addressed to the owner's ACTIVATION CONVERSATION id
2762
+ // (cnv_…), never `account.ownerUserId`: the agent's in-turn reply
2763
+ // inherits this chat_id, and member-backend rejects a usr_… with
2764
+ // "invalid conversation id" — the reply is then dropped server-side
2765
+ // with no negative ack, so the outcome silently never reaches the
2766
+ // owner. With no conversation on record the receipt is skipped
2767
+ // outright (see handlePermissionResult — the dedup slot is preserved
2768
+ // so a replay after activation still renders).
2769
+ const ownerConversationId = resolveOwnerDirectConversationId();
2770
+ if (!ownerConversationId) {
2771
+ log?.info?.(`[${accountId}] clawchat-plugin-openclaw permission_result receipt op=${metadata.operation} outcome=${metadata.outcome} request_id=${metadata.request_id} skipped: no activation conversation recorded`);
2772
+ return;
2773
+ }
2774
+ const note = handlePermissionResult(metadata, account, ownerConversationId);
2662
2775
  if (note) {
2663
2776
  log?.info?.(`[${accountId}] clawchat-plugin-openclaw permission_result receipt op=${metadata.operation} outcome=${metadata.outcome} request_id=${metadata.request_id}; dispatching context note`);
2664
2777
  void handleInboundEnvelope(note);
@@ -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 30 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, 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)");
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
  }
@@ -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
- flush(write) {
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();