@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.
- package/dist/src/api-client.js +3 -0
- package/dist/src/awareness-note-turn.js +27 -1
- package/dist/src/client.js +1 -0
- package/dist/src/liveware-sample.js +272 -41
- package/dist/src/no-reply.js +6 -4
- package/dist/src/outbound.js +81 -3
- package/dist/src/permission-result.js +20 -5
- package/dist/src/protocol-types.js +18 -0
- package/dist/src/reply-dispatcher.js +15 -3
- package/dist/src/runtime.js +125 -12
- 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 +54 -5
- package/src/client.ts +3 -0
- package/src/liveware-sample.ts +287 -41
- package/src/no-reply.ts +6 -4
- package/src/outbound.ts +118 -4
- package/src/permission-result.ts +20 -4
- package/src/protocol-types.ts +19 -0
- package/src/reply-dispatcher.ts +27 -3
- package/src/runtime.ts +145 -12
- 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/friend-request-turn.js +0 -83
package/src/permission-result.ts
CHANGED
|
@@ -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:
|
|
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
|
}
|
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/reply-dispatcher.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
@@ -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" ||
|
|
@@ -2451,11 +2556,19 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2451
2556
|
pendingAwarenessNote = true;
|
|
2452
2557
|
queueMicrotask(() => {
|
|
2453
2558
|
pendingAwarenessNote = false;
|
|
2559
|
+
// No activation conversation ⇒ no routable chat_id. Skip the note
|
|
2560
|
+
// rather than address it to the owner's usr_… id, which
|
|
2561
|
+
// member-backend rejects as "invalid conversation id" (the agent's
|
|
2562
|
+
// in-turn replies are then dropped with no negative ack).
|
|
2563
|
+
const ownerConversationId = resolveOwnerDirectConversationId();
|
|
2564
|
+
if (!ownerConversationId) {
|
|
2565
|
+
log?.info?.(
|
|
2566
|
+
`[${accountId}] clawchat-plugin-openclaw awareness note skipped: no activation conversation recorded`,
|
|
2567
|
+
);
|
|
2568
|
+
return;
|
|
2569
|
+
}
|
|
2454
2570
|
void handleInboundEnvelope(
|
|
2455
|
-
buildAwarenessNoteEnvelope({
|
|
2456
|
-
account,
|
|
2457
|
-
ownerConversationId: resolveOwnerDirectConversationId(),
|
|
2458
|
-
}),
|
|
2571
|
+
buildAwarenessNoteEnvelope({ account, ownerConversationId }),
|
|
2459
2572
|
);
|
|
2460
2573
|
});
|
|
2461
2574
|
}
|
|
@@ -2864,6 +2977,10 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2864
2977
|
updateMessageByIdentity: (input) => store.updateMessageByIdentity?.(input),
|
|
2865
2978
|
}
|
|
2866
2979
|
: null,
|
|
2980
|
+
// Group owner-attention escalations address the owner's activation
|
|
2981
|
+
// conversation, not `ownerUserId` (a usr_… chat_id is rejected by
|
|
2982
|
+
// member-backend). Passed as a thunk so it is resolved at send time.
|
|
2983
|
+
resolveOwnerConversationId: resolveOwnerDirectConversationId,
|
|
2867
2984
|
log,
|
|
2868
2985
|
});
|
|
2869
2986
|
|
|
@@ -3164,7 +3281,23 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
3164
3281
|
// A permission_result receipt closes the loop on an earlier request.
|
|
3165
3282
|
// Dedup by request_id; on a fresh receipt feed the synthetic context
|
|
3166
3283
|
// note back to the agent so it can react to the outcome.
|
|
3167
|
-
|
|
3284
|
+
//
|
|
3285
|
+
// The note is addressed to the owner's ACTIVATION CONVERSATION id
|
|
3286
|
+
// (cnv_…), never `account.ownerUserId`: the agent's in-turn reply
|
|
3287
|
+
// inherits this chat_id, and member-backend rejects a usr_… with
|
|
3288
|
+
// "invalid conversation id" — the reply is then dropped server-side
|
|
3289
|
+
// with no negative ack, so the outcome silently never reaches the
|
|
3290
|
+
// owner. With no conversation on record the receipt is skipped
|
|
3291
|
+
// outright (see handlePermissionResult — the dedup slot is preserved
|
|
3292
|
+
// so a replay after activation still renders).
|
|
3293
|
+
const ownerConversationId = resolveOwnerDirectConversationId();
|
|
3294
|
+
if (!ownerConversationId) {
|
|
3295
|
+
log?.info?.(
|
|
3296
|
+
`[${accountId}] clawchat-plugin-openclaw permission_result receipt op=${metadata.operation} outcome=${metadata.outcome} request_id=${metadata.request_id} skipped: no activation conversation recorded`,
|
|
3297
|
+
);
|
|
3298
|
+
return;
|
|
3299
|
+
}
|
|
3300
|
+
const note = handlePermissionResult(metadata, account, ownerConversationId);
|
|
3168
3301
|
if (note) {
|
|
3169
3302
|
log?.info?.(
|
|
3170
3303
|
`[${accountId}] clawchat-plugin-openclaw permission_result receipt op=${metadata.operation} outcome=${metadata.outcome} request_id=${metadata.request_id}; dispatching context note`,
|
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();
|