@clawling/clawchat-plugin-openclaw 2026.7.29-1 → 2026.7.29-3

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.
@@ -16,7 +16,7 @@ export function buildAwarenessNoteEnvelope(params) {
16
16
  event: EVENT.MESSAGE_SEND,
17
17
  trace_id: `clawchat-plugin-openclaw-awareness-${now}`,
18
18
  emitted_at: now,
19
- chat_id: ownerConversationId ?? account.ownerUserId,
19
+ chat_id: ownerConversationId,
20
20
  chat_type: "direct",
21
21
  to: { id: account.userId, type: "direct" },
22
22
  sender: { id: "clawchat-awareness", type: "direct", nick_name: "ClawChat" },
@@ -8,10 +8,12 @@
8
8
  * anchored, wrapping decorations (`<>`, `[]`, `{}`, `/`, backticks, `*`) are
9
9
  * tolerated for free and must not be written into the pattern itself.
10
10
  *
11
- * RULE B — the host's bare markers (`NO_REPLY` / `[SILENT]` / `SILENT` /
12
- * `NO REPLY`), matched as a WHOLE STRING ONLY. These are ordinary English
13
- * words; substring-matching them would swallow prose such as "there is no reply
14
- * from the server".
11
+ * RULE B — bare runtime silence markers (`NO_REPLY` / `[SILENT]` / `SILENT` /
12
+ * `NO REPLY`), matched as a WHOLE STRING ONLY. OpenClaw itself has no such
13
+ * convention; these are carried because other agent runtimes define them, so a
14
+ * model may fall back to one instead of the `clawchat:` form. They are ordinary
15
+ * English words — substring-matching them would swallow prose such as "there is
16
+ * no reply from the server".
15
17
  *
16
18
  * This module MUST stay a literal mirror of the Hermes plugin's equivalent
17
19
  * module. When one side changes, change the other in the same breath.
@@ -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
  }
@@ -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({
@@ -2028,10 +2028,16 @@ export async function startOpenclawClawlingGateway(params) {
2028
2028
  pendingAwarenessNote = true;
2029
2029
  queueMicrotask(() => {
2030
2030
  pendingAwarenessNote = false;
2031
- void handleInboundEnvelope(buildAwarenessNoteEnvelope({
2032
- account,
2033
- ownerConversationId: resolveOwnerDirectConversationId(),
2034
- }));
2031
+ // No activation conversation ⇒ no routable chat_id. Skip the note
2032
+ // rather than address it to the owner's usr_… id, which
2033
+ // member-backend rejects as "invalid conversation id" (the agent's
2034
+ // in-turn replies are then dropped with no negative ack).
2035
+ const ownerConversationId = resolveOwnerDirectConversationId();
2036
+ if (!ownerConversationId) {
2037
+ log?.info?.(`[${accountId}] clawchat-plugin-openclaw awareness note skipped: no activation conversation recorded`);
2038
+ return;
2039
+ }
2040
+ void handleInboundEnvelope(buildAwarenessNoteEnvelope({ account, ownerConversationId }));
2035
2041
  });
2036
2042
  }
2037
2043
  }
@@ -2405,6 +2411,10 @@ export async function startOpenclawClawlingGateway(params) {
2405
2411
  updateMessageByIdentity: (input) => store.updateMessageByIdentity?.(input),
2406
2412
  }
2407
2413
  : null,
2414
+ // Group owner-attention escalations address the owner's activation
2415
+ // conversation, not `ownerUserId` (a usr_… chat_id is rejected by
2416
+ // member-backend). Passed as a thunk so it is resolved at send time.
2417
+ resolveOwnerConversationId: resolveOwnerDirectConversationId,
2408
2418
  log,
2409
2419
  });
2410
2420
  const agentsConfigured = Object.keys(cfg.agents ?? {});
@@ -2658,7 +2668,21 @@ export async function startOpenclawClawlingGateway(params) {
2658
2668
  // A permission_result receipt closes the loop on an earlier request.
2659
2669
  // Dedup by request_id; on a fresh receipt feed the synthetic context
2660
2670
  // note back to the agent so it can react to the outcome.
2661
- const note = handlePermissionResult(metadata, account);
2671
+ //
2672
+ // The note is addressed to the owner's ACTIVATION CONVERSATION id
2673
+ // (cnv_…), never `account.ownerUserId`: the agent's in-turn reply
2674
+ // inherits this chat_id, and member-backend rejects a usr_… with
2675
+ // "invalid conversation id" — the reply is then dropped server-side
2676
+ // with no negative ack, so the outcome silently never reaches the
2677
+ // owner. With no conversation on record the receipt is skipped
2678
+ // outright (see handlePermissionResult — the dedup slot is preserved
2679
+ // so a replay after activation still renders).
2680
+ const ownerConversationId = resolveOwnerDirectConversationId();
2681
+ if (!ownerConversationId) {
2682
+ 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`);
2683
+ return;
2684
+ }
2685
+ const note = handlePermissionResult(metadata, account, ownerConversationId);
2662
2686
  if (note) {
2663
2687
  log?.info?.(`[${accountId}] clawchat-plugin-openclaw permission_result receipt op=${metadata.operation} outcome=${metadata.outcome} request_id=${metadata.request_id}; dispatching context note`);
2664
2688
  void handleInboundEnvelope(note);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawling/clawchat-plugin-openclaw",
3
- "version": "2026.7.29-1",
3
+ "version": "2026.7.29-3",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "license": "MIT",
6
6
  "author": "CLAWLING PTE. LTD.",
@@ -13,11 +13,15 @@ import type { ResolvedOpenclawClawlingAccount } from "./config.ts";
13
13
  export interface BuildAwarenessNoteEnvelopeParams {
14
14
  account: ResolvedOpenclawClawlingAccount;
15
15
  /**
16
- * The owner's direct conversation id (cnv_…) recorded at activation, or
17
- * null when none is recorded (the note still runs, but replies will not be
18
- * deliverable until activation records the owner conversation).
16
+ * The owner's direct conversation id (cnv_…) recorded at activation.
17
+ *
18
+ * Required and non-null: there is no valid fallback. Addressing the note to
19
+ * `account.ownerUserId` instead makes the agent's in-turn replies unroutable
20
+ * (member-backend answers "invalid conversation id"), so callers must skip
21
+ * the note entirely while no conversation is on record rather than hand a
22
+ * usr_… id down here.
19
23
  */
20
- ownerConversationId: string | null;
24
+ ownerConversationId: string;
21
25
  }
22
26
 
23
27
  export function buildAwarenessNoteEnvelope(
@@ -30,7 +34,7 @@ export function buildAwarenessNoteEnvelope(
30
34
  event: EVENT.MESSAGE_SEND,
31
35
  trace_id: `clawchat-plugin-openclaw-awareness-${now}`,
32
36
  emitted_at: now,
33
- chat_id: ownerConversationId ?? account.ownerUserId,
37
+ chat_id: ownerConversationId,
34
38
  chat_type: "direct",
35
39
  to: { id: account.userId, type: "direct" },
36
40
  sender: { id: "clawchat-awareness", type: "direct", nick_name: "ClawChat" },
package/src/no-reply.ts CHANGED
@@ -8,10 +8,12 @@
8
8
  * anchored, wrapping decorations (`<>`, `[]`, `{}`, `/`, backticks, `*`) are
9
9
  * tolerated for free and must not be written into the pattern itself.
10
10
  *
11
- * RULE B — the host's bare markers (`NO_REPLY` / `[SILENT]` / `SILENT` /
12
- * `NO REPLY`), matched as a WHOLE STRING ONLY. These are ordinary English
13
- * words; substring-matching them would swallow prose such as "there is no reply
14
- * from the server".
11
+ * RULE B — bare runtime silence markers (`NO_REPLY` / `[SILENT]` / `SILENT` /
12
+ * `NO REPLY`), matched as a WHOLE STRING ONLY. OpenClaw itself has no such
13
+ * convention; these are carried because other agent runtimes define them, so a
14
+ * model may fall back to one instead of the `clawchat:` form. They are ordinary
15
+ * English words — substring-matching them would swallow prose such as "there is
16
+ * no reply from the server".
15
17
  *
16
18
  * This module MUST stay a literal mirror of the Hermes plugin's equivalent
17
19
  * module. When one side changes, change the other in the same breath.
@@ -79,12 +79,20 @@ function buildOutcomeNote(metadata: PermissionResultMetadata): string {
79
79
  * Mirrors the `buildAwarenessNoteEnvelope` pattern: a synthetic inbound
80
80
  * `message.send` targeting the owner's direct conversation, routed through
81
81
  * `handleInboundEnvelope` so the agent can reason about the outcome.
82
+ *
83
+ * `ownerConversationId` MUST be the owner's direct conversation id (`cnv_…`)
84
+ * recorded at activation, never `account.ownerUserId`. The agent's in-turn
85
+ * reply inherits this note's `chat_id`, and msghub resolves chat_id through
86
+ * member-backend — a `usr_…` there comes back "invalid conversation id", so the
87
+ * reply is dropped and the sender never learns. Hence the required non-null
88
+ * parameter: there is no safe value to fall back to.
82
89
  */
83
90
  export function buildPermissionResultEnvelope(params: {
84
91
  account: Pick<ResolvedOpenclawClawlingAccount, "userId" | "ownerUserId">;
85
92
  metadata: PermissionResultMetadata;
93
+ ownerConversationId: string;
86
94
  }): Envelope {
87
- const { account, metadata } = params;
95
+ const { account, metadata, ownerConversationId } = params;
88
96
  const text = buildOutcomeNote(metadata);
89
97
  const now = Date.now();
90
98
  return {
@@ -92,7 +100,7 @@ export function buildPermissionResultEnvelope(params: {
92
100
  event: EVENT.MESSAGE_SEND,
93
101
  trace_id: `clawchat-plugin-openclaw-permission-result-${metadata.request_id}-${now}`,
94
102
  emitted_at: now,
95
- chat_id: account.ownerUserId,
103
+ chat_id: ownerConversationId,
96
104
  chat_type: "direct",
97
105
  to: { id: account.userId, type: "direct" },
98
106
  sender: {
@@ -128,13 +136,21 @@ export function buildPermissionResultEnvelope(params: {
128
136
  *
129
137
  * Returns a synthetic context-note {@link Envelope} on a fresh receipt so
130
138
  * the caller can feed it to `handleInboundEnvelope`; returns `null` when
131
- * the `request_id` has already been processed.
139
+ * the `request_id` has already been processed, or when `ownerConversationId`
140
+ * is absent.
141
+ *
142
+ * A null `ownerConversationId` (activation has not recorded the owner's direct
143
+ * conversation yet) is checked BEFORE the dedup mark on purpose: the receipt
144
+ * cannot be routed anywhere, so burning its dedup slot would permanently
145
+ * swallow it — the replayed copy must still render once the id is known.
132
146
  */
133
147
  export function handlePermissionResult(
134
148
  metadata: PermissionResultMetadata,
135
149
  account: Pick<ResolvedOpenclawClawlingAccount, "userId" | "ownerUserId">,
150
+ ownerConversationId: string | null,
136
151
  ): Envelope | null {
152
+ if (!ownerConversationId) return null;
137
153
  if (metadata.request_id && _seenRequestIds.has(metadata.request_id)) return null;
138
154
  if (metadata.request_id) _seenRequestIds.add(metadata.request_id);
139
- return buildPermissionResultEnvelope({ account, metadata });
155
+ return buildPermissionResultEnvelope({ account, metadata, ownerConversationId });
140
156
  }
@@ -59,6 +59,18 @@ export interface ReplyDispatcherOptions {
59
59
  ClawChatStore,
60
60
  "insertMessage" | "claimMessageOnce" | "markMessageAcknowledged" | "updateMessageByIdentity"
61
61
  > | null;
62
+ /**
63
+ * Resolves the owner's direct conversation id (`cnv_…`) recorded at
64
+ * activation, or null when none is on record. Used only by the group
65
+ * owner-attention escalation, which must address a conversation id — NOT
66
+ * `account.ownerUserId`. member-backend rejects a `usr_…` chat_id with
67
+ * "invalid conversation id", so such an escalation is dropped server-side
68
+ * and the owner never sees it.
69
+ *
70
+ * Called lazily at send time, so an activation that lands after this
71
+ * dispatcher was constructed is still picked up.
72
+ */
73
+ resolveOwnerConversationId?: () => string | null;
62
74
  log?: { info?: (m: string) => void; error?: (m: string) => void };
63
75
  }
64
76
 
@@ -387,15 +399,24 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
387
399
  replyCtx,
388
400
  inboundMessageId,
389
401
  store,
402
+ resolveOwnerConversationId,
390
403
  log,
391
404
  } = options;
392
405
  const isGroupTarget = target.chatType === "group";
393
406
  const outputVisibility = effectiveOutputVisibility(account, target.chatId, target.chatType);
394
407
  const splitFullOutput = outputVisibility === "full";
395
408
  const splitNormalBlockOutput = outputVisibility === "normal";
396
- const ownerDirectTarget = () => {
409
+ // The escalation target is the owner's ACTIVATION CONVERSATION id, never the
410
+ // owner's usr_… id: chat_id is resolved by msghub through member-backend, and
411
+ // a usr_… comes back "invalid conversation id" — the frame is dropped with no
412
+ // negative ack to this client, so the owner silently never gets the approval
413
+ // request. Both ids are required: ownerUserId gates whether an owner exists at
414
+ // all, the conversation id is where a frame can actually be delivered.
415
+ const ownerDirectTarget = (): { chatId: string; chatType: "direct" } | null => {
397
416
  const ownerUserId = account.ownerUserId?.trim();
398
- return ownerUserId ? { chatId: ownerUserId, chatType: "direct" as const } : null;
417
+ if (!ownerUserId) return null;
418
+ const conversationId = resolveOwnerConversationId?.()?.trim();
419
+ return conversationId ? { chatId: conversationId, chatType: "direct" as const } : null;
399
420
  };
400
421
  const humanDelay = runtime.channel.reply.resolveHumanDelayConfig(cfg, account.userId);
401
422
 
@@ -585,8 +606,11 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
585
606
  ): Promise<void> => {
586
607
  const ownerTarget = ownerDirectTarget();
587
608
  if (!ownerTarget) {
609
+ const reason = account.ownerUserId?.trim()
610
+ ? "missing_owner_conversation"
611
+ : "missing_owner_user_id";
588
612
  log?.error?.(
589
- `[${account.accountId}] clawchat-plugin-openclaw group owner attention suppressed reason=missing_owner_user_id group=${target.chatId}`,
613
+ `[${account.accountId}] clawchat-plugin-openclaw group owner attention suppressed reason=${reason} group=${target.chatId}`,
590
614
  );
591
615
  return;
592
616
  }
package/src/runtime.ts CHANGED
@@ -2451,11 +2451,19 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
2451
2451
  pendingAwarenessNote = true;
2452
2452
  queueMicrotask(() => {
2453
2453
  pendingAwarenessNote = false;
2454
+ // No activation conversation ⇒ no routable chat_id. Skip the note
2455
+ // rather than address it to the owner's usr_… id, which
2456
+ // member-backend rejects as "invalid conversation id" (the agent's
2457
+ // in-turn replies are then dropped with no negative ack).
2458
+ const ownerConversationId = resolveOwnerDirectConversationId();
2459
+ if (!ownerConversationId) {
2460
+ log?.info?.(
2461
+ `[${accountId}] clawchat-plugin-openclaw awareness note skipped: no activation conversation recorded`,
2462
+ );
2463
+ return;
2464
+ }
2454
2465
  void handleInboundEnvelope(
2455
- buildAwarenessNoteEnvelope({
2456
- account,
2457
- ownerConversationId: resolveOwnerDirectConversationId(),
2458
- }),
2466
+ buildAwarenessNoteEnvelope({ account, ownerConversationId }),
2459
2467
  );
2460
2468
  });
2461
2469
  }
@@ -2864,6 +2872,10 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
2864
2872
  updateMessageByIdentity: (input) => store.updateMessageByIdentity?.(input),
2865
2873
  }
2866
2874
  : null,
2875
+ // Group owner-attention escalations address the owner's activation
2876
+ // conversation, not `ownerUserId` (a usr_… chat_id is rejected by
2877
+ // member-backend). Passed as a thunk so it is resolved at send time.
2878
+ resolveOwnerConversationId: resolveOwnerDirectConversationId,
2867
2879
  log,
2868
2880
  });
2869
2881
 
@@ -3164,7 +3176,23 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
3164
3176
  // A permission_result receipt closes the loop on an earlier request.
3165
3177
  // Dedup by request_id; on a fresh receipt feed the synthetic context
3166
3178
  // note back to the agent so it can react to the outcome.
3167
- const note = handlePermissionResult(metadata, account);
3179
+ //
3180
+ // The note is addressed to the owner's ACTIVATION CONVERSATION id
3181
+ // (cnv_…), never `account.ownerUserId`: the agent's in-turn reply
3182
+ // inherits this chat_id, and member-backend rejects a usr_… with
3183
+ // "invalid conversation id" — the reply is then dropped server-side
3184
+ // with no negative ack, so the outcome silently never reaches the
3185
+ // owner. With no conversation on record the receipt is skipped
3186
+ // outright (see handlePermissionResult — the dedup slot is preserved
3187
+ // so a replay after activation still renders).
3188
+ const ownerConversationId = resolveOwnerDirectConversationId();
3189
+ if (!ownerConversationId) {
3190
+ log?.info?.(
3191
+ `[${accountId}] clawchat-plugin-openclaw permission_result receipt op=${metadata.operation} outcome=${metadata.outcome} request_id=${metadata.request_id} skipped: no activation conversation recorded`,
3192
+ );
3193
+ return;
3194
+ }
3195
+ const note = handlePermissionResult(metadata, account, ownerConversationId);
3168
3196
  if (note) {
3169
3197
  log?.info?.(
3170
3198
  `[${accountId}] clawchat-plugin-openclaw permission_result receipt op=${metadata.operation} outcome=${metadata.outcome} request_id=${metadata.request_id}; dispatching context note`,
@@ -1,83 +0,0 @@
1
- /**
2
- * Synthetic reasoning-turn builder for `friend.request` notify.signal events.
3
- *
4
- * When the server fires a `friend.request` signal the runtime dispatches one
5
- * deduped synthetic `message.send` envelope so the agent can reason about the
6
- * pending request. The prompt text varies by the current "friend.add" permission
7
- * policy state: a deny policy yields a decline/inform prompt; ask or allow
8
- * yields a prompt that instructs the agent to review the request via the
9
- * `clawchat_list_friend_requests` tool.
10
- */
11
- import { EVENT } from "./protocol-types.js";
12
- /**
13
- * Returns canned prompt text for a friend-request reasoning turn based on the
14
- * current "friend.add" permission policy state.
15
- *
16
- * - `deny` → inform the agent it should decline; no accept instruction.
17
- * - `ask` / `allow` → instruct the agent to review the pending request.
18
- *
19
- * The requester's user id is embedded so the agent does not go looking up the
20
- * synthetic sender ("clawchat-friend-request" is not a real user).
21
- */
22
- export function friendRequestPromptFor(state, requesterUserId) {
23
- const arrived = requesterUserId
24
- ? `A new friend request has arrived from user ${requesterUserId}.`
25
- : "A new friend request has arrived.";
26
- if (state === "deny") {
27
- return [
28
- arrived,
29
- "Your current friend-add policy is set to deny.",
30
- "Do not add this contact.",
31
- "You may inform the requester that you cannot add them at this time.",
32
- ].join(" ");
33
- }
34
- return [
35
- arrived,
36
- "Please review the pending request by calling `clawchat_list_friend_requests`",
37
- "and decide whether to accept it.",
38
- ].join(" ");
39
- }
40
- /**
41
- * Builds a synthetic `message.send` envelope that triggers one agent reasoning
42
- * turn for a pending friend request. The envelope shape mirrors the
43
- * activation-bootstrap envelope; it targets the owner's direct conversation so
44
- * the agent has context about who it is reasoning for.
45
- */
46
- export function buildFriendRequestEnvelope(params) {
47
- const { account, state, entityId, ownerConversationId } = params;
48
- const text = friendRequestPromptFor(state, entityId);
49
- const now = Date.now();
50
- // Fall back to ownerUserId only when no activation conversation is recorded:
51
- // the turn still runs (the agent can act via tools, e.g. accept the request),
52
- // but its in-chat replies will not be deliverable until activation records
53
- // the owner conversation.
54
- return {
55
- version: "2",
56
- event: EVENT.MESSAGE_SEND,
57
- trace_id: `clawchat-plugin-openclaw-friend-request-${entityId}-${now}`,
58
- emitted_at: now,
59
- chat_id: ownerConversationId ?? account.ownerUserId,
60
- chat_type: "direct",
61
- to: { id: account.userId, type: "direct" },
62
- sender: {
63
- id: "clawchat-friend-request",
64
- type: "direct",
65
- nick_name: "ClawChat",
66
- },
67
- payload: {
68
- message_id: `clawchat-plugin-openclaw-friend-request-${entityId}-${now}`,
69
- message_mode: "normal",
70
- message: {
71
- body: { fragments: [{ kind: "text", text }] },
72
- context: { mentions: [], reply: null },
73
- streaming: {
74
- status: "static",
75
- sequence: 0,
76
- mutation_policy: "sealed",
77
- started_at: null,
78
- completed_at: null,
79
- },
80
- },
81
- },
82
- };
83
- }