@clawling/clawchat-plugin-openclaw 2026.8.20-1 → 2026.8.22-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/protocol-types.js +18 -0
- package/dist/src/runtime.js +28 -1
- package/dist/src/storage.js +32 -0
- package/dist/src/ws-client.js +5 -0
- package/package.json +1 -1
- package/src/protocol-types.ts +37 -0
- package/src/runtime.ts +34 -0
- package/src/storage.ts +38 -0
- package/src/ws-client.ts +4 -0
|
@@ -8,6 +8,7 @@ export const EVENT = {
|
|
|
8
8
|
MESSAGE_ERROR: "message.error",
|
|
9
9
|
MESSAGE_REPLY: "message.reply",
|
|
10
10
|
MESSAGE_REACTION: "message.reaction",
|
|
11
|
+
MESSAGE_RECALL: "message.recall",
|
|
11
12
|
MESSAGE_CREATED: "message.created",
|
|
12
13
|
MESSAGE_ADD: "message.add",
|
|
13
14
|
MESSAGE_DONE: "message.done",
|
|
@@ -90,3 +91,20 @@ export class StateError extends Error {
|
|
|
90
91
|
export function isBusinessDispatchEvent(event) {
|
|
91
92
|
return event === EVENT.MESSAGE_SEND || event === EVENT.MESSAGE_REPLY;
|
|
92
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Pull the id to delete out of a `message.recall` payload.
|
|
96
|
+
*
|
|
97
|
+
* Exists as a pure function, apart from the socket, for the same reason the
|
|
98
|
+
* mistake it guards is worth guarding: `payload.message_id` is right there and
|
|
99
|
+
* looks like the answer, but it is the `rcl:` slot. Deleting by it removes
|
|
100
|
+
* nothing and reads exactly like working code.
|
|
101
|
+
*
|
|
102
|
+
* Answers `null` for anything unusable, so a recall we cannot resolve deletes
|
|
103
|
+
* nothing rather than something adjacent.
|
|
104
|
+
*/
|
|
105
|
+
export function recallTargetMessageId(payload) {
|
|
106
|
+
if (!payload || typeof payload !== "object")
|
|
107
|
+
return null;
|
|
108
|
+
const target = payload.target_message_id;
|
|
109
|
+
return typeof target === "string" && target.length > 0 ? target : null;
|
|
110
|
+
}
|
package/dist/src/runtime.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AckTimeoutError, AuthError, ProtocolError, StateError, TransportError, EVENT, } from "./protocol-types.js";
|
|
1
|
+
import { AckTimeoutError, AuthError, ProtocolError, StateError, TransportError, EVENT, recallTargetMessageId, } from "./protocol-types.js";
|
|
2
2
|
import { waitUntilAbort } from "openclaw/plugin-sdk/channel-lifecycle";
|
|
3
3
|
import { hasControlCommand } from "openclaw/plugin-sdk/command-detection";
|
|
4
4
|
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
@@ -2907,6 +2907,33 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
2907
2907
|
await handleInboundEnvelope(env);
|
|
2908
2908
|
})();
|
|
2909
2909
|
});
|
|
2910
|
+
// `message.recall` — the sender withdrew a message. Erase our own ledger row
|
|
2911
|
+
// so the "Prior group context" block above (MENTION_CONTEXT_N rows, no time
|
|
2912
|
+
// predicate, no retention) can never read it back into a turn.
|
|
2913
|
+
//
|
|
2914
|
+
// Ids only: the frame carries no content, and `payload.message_id` is the
|
|
2915
|
+
// server's `rcl:<target>` slot — the id to delete is `target_message_id`.
|
|
2916
|
+
//
|
|
2917
|
+
// Synchronous and fire-and-forget on purpose. There is no ack to send (the
|
|
2918
|
+
// frame is BestEffort and non-ackable), and no tombstone to write: a Kafka
|
|
2919
|
+
// redelivery of the original can resurrect the row, which is an accepted
|
|
2920
|
+
// known limitation rather than a fourth reimplementation of the mobile
|
|
2921
|
+
// client's tombstone table.
|
|
2922
|
+
client.on("message:recall", (env) => {
|
|
2923
|
+
const target = recallTargetMessageId(env.payload);
|
|
2924
|
+
if (!target) {
|
|
2925
|
+
log?.info?.(`[${accountId}] clawchat-plugin-openclaw recall ignored (no target_message_id) chat_id=${String(env.chat_id ?? "")}`);
|
|
2926
|
+
return;
|
|
2927
|
+
}
|
|
2928
|
+
const removed = store?.deleteMessagesByMessageId?.({
|
|
2929
|
+
platform: "openclaw",
|
|
2930
|
+
accountId,
|
|
2931
|
+
messageId: target,
|
|
2932
|
+
});
|
|
2933
|
+
const rowsLabel = typeof removed === "number" ? String(removed) : "unavailable";
|
|
2934
|
+
const logFn = typeof removed === "number" ? log?.info : log?.error;
|
|
2935
|
+
logFn?.(`[${accountId}] clawchat-plugin-openclaw recall purged chat_id=${String(env.chat_id ?? "")} msg=${target} rows=${rowsLabel}`);
|
|
2936
|
+
});
|
|
2910
2937
|
// Initial pull of per-group agent settings at startup (best-effort; errors are
|
|
2911
2938
|
// logged). `bypassRefresh: true` (Finding 4) keeps this pre-connect pull from
|
|
2912
2939
|
// rotating `account.token` out from under the already-constructed WS client.
|
package/dist/src/storage.js
CHANGED
|
@@ -443,6 +443,38 @@ export class ClawChatStore {
|
|
|
443
443
|
.run(input.eventType, input.traceId ?? null, input.chatId ?? null, input.text ?? null, toJson(input.raw), input.accountId, input.kind, input.direction, input.messageId);
|
|
444
444
|
});
|
|
445
445
|
}
|
|
446
|
+
/**
|
|
447
|
+
* Erase every ledger row for one `message_id` — the local half of a
|
|
448
|
+
* server-authorised recall (`message.recall`, docs/client-integration.md §9.8).
|
|
449
|
+
*
|
|
450
|
+
* **The only DELETE in this store, and deliberately so.** There is no prune
|
|
451
|
+
* and no retention on `clawchat_messages`: without this, a message recalled
|
|
452
|
+
* ninety seconds after it was sent stays one @-mention away from being read
|
|
453
|
+
* back aloud under "Prior group context" for as long as the group is quiet.
|
|
454
|
+
*
|
|
455
|
+
* Scoped to `(platform, account_id)` so one account's recall cannot reach
|
|
456
|
+
* another's rows, and matched on `message_id`, which seeks via the
|
|
457
|
+
* single-column `idx_clawchat_messages_message_id` before the `platform` /
|
|
458
|
+
* `account_id` filter is applied — so this is a lookup, not a table scan.
|
|
459
|
+
*
|
|
460
|
+
* Answers the number of rows removed. `0` is a normal outcome, not a failure:
|
|
461
|
+
* the row may never have been stored (a message that arrived before this
|
|
462
|
+
* account was paired, or one dropped by the inbound filter). `null` means the
|
|
463
|
+
* store is unavailable.
|
|
464
|
+
*/
|
|
465
|
+
deleteMessagesByMessageId(input) {
|
|
466
|
+
// An empty id would match every row whose message_id is '' — deleting
|
|
467
|
+
// everything the store never managed to key. Refuse rather than guess.
|
|
468
|
+
if (!input.messageId)
|
|
469
|
+
return 0;
|
|
470
|
+
return this.write(() => {
|
|
471
|
+
const result = this.requireDb()
|
|
472
|
+
.prepare(`DELETE FROM clawchat_messages
|
|
473
|
+
WHERE platform = ? AND account_id = ? AND message_id = ?`)
|
|
474
|
+
.run(input.platform, input.accountId, input.messageId);
|
|
475
|
+
return Number(result.changes);
|
|
476
|
+
});
|
|
477
|
+
}
|
|
446
478
|
startConnection(input) {
|
|
447
479
|
return this.write(() => {
|
|
448
480
|
const now = input.connectStartedAt ?? Date.now();
|
package/dist/src/ws-client.js
CHANGED
|
@@ -558,6 +558,11 @@ export class ClawChatClient extends EventEmitter {
|
|
|
558
558
|
return this.onMessageError(env);
|
|
559
559
|
if (isBusinessDispatchEvent(env.event))
|
|
560
560
|
this.emit("message", env);
|
|
561
|
+
// Deliberately NOT a business dispatch event: `message` feeds the turn
|
|
562
|
+
// pipeline, and a recall has no body to turn into a prompt. It is an
|
|
563
|
+
// instruction to delete, so it gets its own emitter and its own consumer.
|
|
564
|
+
if (env.event === EVENT.MESSAGE_RECALL)
|
|
565
|
+
this.emit("message:recall", env);
|
|
561
566
|
if (env.event === EVENT.MESSAGE_CREATED)
|
|
562
567
|
this.emit("message:created", env);
|
|
563
568
|
if (env.event === EVENT.MESSAGE_ADD)
|
package/package.json
CHANGED
package/src/protocol-types.ts
CHANGED
|
@@ -8,6 +8,7 @@ export const EVENT = {
|
|
|
8
8
|
MESSAGE_ERROR: "message.error",
|
|
9
9
|
MESSAGE_REPLY: "message.reply",
|
|
10
10
|
MESSAGE_REACTION: "message.reaction",
|
|
11
|
+
MESSAGE_RECALL: "message.recall",
|
|
11
12
|
MESSAGE_CREATED: "message.created",
|
|
12
13
|
MESSAGE_ADD: "message.add",
|
|
13
14
|
MESSAGE_DONE: "message.done",
|
|
@@ -361,3 +362,39 @@ export class StateError extends Error {
|
|
|
361
362
|
export function isBusinessDispatchEvent(event: string): boolean {
|
|
362
363
|
return event === EVENT.MESSAGE_SEND || event === EVENT.MESSAGE_REPLY;
|
|
363
364
|
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* `message.recall` downlink payload (docs/client-integration.md §9.8).
|
|
368
|
+
*
|
|
369
|
+
* **Ids only, never content.** The frame relays an instruction to delete, not
|
|
370
|
+
* a copy of what is being deleted — which is what makes it compatible with the
|
|
371
|
+
* feature's "no trace anywhere" requirement.
|
|
372
|
+
*/
|
|
373
|
+
export interface MessageRecallPayload {
|
|
374
|
+
/**
|
|
375
|
+
* The server's deterministic slot id, `rcl:<target_message_id>`. Present so
|
|
376
|
+
* the marker can occupy one durable inbox row per recalled message. It is
|
|
377
|
+
* NOT the id to delete.
|
|
378
|
+
*/
|
|
379
|
+
message_id: string;
|
|
380
|
+
|
|
381
|
+
/** The id of the message being erased. This is the one to delete by. */
|
|
382
|
+
target_message_id: string;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Pull the id to delete out of a `message.recall` payload.
|
|
387
|
+
*
|
|
388
|
+
* Exists as a pure function, apart from the socket, for the same reason the
|
|
389
|
+
* mistake it guards is worth guarding: `payload.message_id` is right there and
|
|
390
|
+
* looks like the answer, but it is the `rcl:` slot. Deleting by it removes
|
|
391
|
+
* nothing and reads exactly like working code.
|
|
392
|
+
*
|
|
393
|
+
* Answers `null` for anything unusable, so a recall we cannot resolve deletes
|
|
394
|
+
* nothing rather than something adjacent.
|
|
395
|
+
*/
|
|
396
|
+
export function recallTargetMessageId(payload: unknown): string | null {
|
|
397
|
+
if (!payload || typeof payload !== "object") return null;
|
|
398
|
+
const target = (payload as { target_message_id?: unknown }).target_message_id;
|
|
399
|
+
return typeof target === "string" && target.length > 0 ? target : null;
|
|
400
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
StateError,
|
|
6
6
|
TransportError,
|
|
7
7
|
EVENT,
|
|
8
|
+
recallTargetMessageId,
|
|
8
9
|
type Envelope,
|
|
9
10
|
type Transport,
|
|
10
11
|
} from "./protocol-types.ts";
|
|
@@ -123,6 +124,7 @@ type RuntimeConnectionStore = Pick<
|
|
|
123
124
|
| "getActivationConversation"
|
|
124
125
|
| "getLastResolvedDeviceId"
|
|
125
126
|
| "listRecentGroupMessages"
|
|
127
|
+
| "deleteMessagesByMessageId"
|
|
126
128
|
| "upsertOwnerProfile"
|
|
127
129
|
>
|
|
128
130
|
>;
|
|
@@ -3447,6 +3449,38 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
3447
3449
|
})();
|
|
3448
3450
|
});
|
|
3449
3451
|
|
|
3452
|
+
// `message.recall` — the sender withdrew a message. Erase our own ledger row
|
|
3453
|
+
// so the "Prior group context" block above (MENTION_CONTEXT_N rows, no time
|
|
3454
|
+
// predicate, no retention) can never read it back into a turn.
|
|
3455
|
+
//
|
|
3456
|
+
// Ids only: the frame carries no content, and `payload.message_id` is the
|
|
3457
|
+
// server's `rcl:<target>` slot — the id to delete is `target_message_id`.
|
|
3458
|
+
//
|
|
3459
|
+
// Synchronous and fire-and-forget on purpose. There is no ack to send (the
|
|
3460
|
+
// frame is BestEffort and non-ackable), and no tombstone to write: a Kafka
|
|
3461
|
+
// redelivery of the original can resurrect the row, which is an accepted
|
|
3462
|
+
// known limitation rather than a fourth reimplementation of the mobile
|
|
3463
|
+
// client's tombstone table.
|
|
3464
|
+
client.on("message:recall", (env: Envelope) => {
|
|
3465
|
+
const target = recallTargetMessageId(env.payload);
|
|
3466
|
+
if (!target) {
|
|
3467
|
+
log?.info?.(
|
|
3468
|
+
`[${accountId}] clawchat-plugin-openclaw recall ignored (no target_message_id) chat_id=${String(env.chat_id ?? "")}`,
|
|
3469
|
+
);
|
|
3470
|
+
return;
|
|
3471
|
+
}
|
|
3472
|
+
const removed = store?.deleteMessagesByMessageId?.({
|
|
3473
|
+
platform: "openclaw",
|
|
3474
|
+
accountId,
|
|
3475
|
+
messageId: target,
|
|
3476
|
+
});
|
|
3477
|
+
const rowsLabel = typeof removed === "number" ? String(removed) : "unavailable";
|
|
3478
|
+
const logFn = typeof removed === "number" ? log?.info : log?.error;
|
|
3479
|
+
logFn?.(
|
|
3480
|
+
`[${accountId}] clawchat-plugin-openclaw recall purged chat_id=${String(env.chat_id ?? "")} msg=${target} rows=${rowsLabel}`,
|
|
3481
|
+
);
|
|
3482
|
+
});
|
|
3483
|
+
|
|
3450
3484
|
// Initial pull of per-group agent settings at startup (best-effort; errors are
|
|
3451
3485
|
// logged). `bypassRefresh: true` (Finding 4) keeps this pre-connect pull from
|
|
3452
3486
|
// rotating `account.token` out from under the already-constructed WS client.
|
package/src/storage.ts
CHANGED
|
@@ -802,6 +802,44 @@ export class ClawChatStore {
|
|
|
802
802
|
});
|
|
803
803
|
}
|
|
804
804
|
|
|
805
|
+
/**
|
|
806
|
+
* Erase every ledger row for one `message_id` — the local half of a
|
|
807
|
+
* server-authorised recall (`message.recall`, docs/client-integration.md §9.8).
|
|
808
|
+
*
|
|
809
|
+
* **The only DELETE in this store, and deliberately so.** There is no prune
|
|
810
|
+
* and no retention on `clawchat_messages`: without this, a message recalled
|
|
811
|
+
* ninety seconds after it was sent stays one @-mention away from being read
|
|
812
|
+
* back aloud under "Prior group context" for as long as the group is quiet.
|
|
813
|
+
*
|
|
814
|
+
* Scoped to `(platform, account_id)` so one account's recall cannot reach
|
|
815
|
+
* another's rows, and matched on `message_id`, which seeks via the
|
|
816
|
+
* single-column `idx_clawchat_messages_message_id` before the `platform` /
|
|
817
|
+
* `account_id` filter is applied — so this is a lookup, not a table scan.
|
|
818
|
+
*
|
|
819
|
+
* Answers the number of rows removed. `0` is a normal outcome, not a failure:
|
|
820
|
+
* the row may never have been stored (a message that arrived before this
|
|
821
|
+
* account was paired, or one dropped by the inbound filter). `null` means the
|
|
822
|
+
* store is unavailable.
|
|
823
|
+
*/
|
|
824
|
+
deleteMessagesByMessageId(input: {
|
|
825
|
+
platform: string;
|
|
826
|
+
accountId: string;
|
|
827
|
+
messageId: string;
|
|
828
|
+
}): number | null {
|
|
829
|
+
// An empty id would match every row whose message_id is '' — deleting
|
|
830
|
+
// everything the store never managed to key. Refuse rather than guess.
|
|
831
|
+
if (!input.messageId) return 0;
|
|
832
|
+
return this.write(() => {
|
|
833
|
+
const result = this.requireDb()
|
|
834
|
+
.prepare(
|
|
835
|
+
`DELETE FROM clawchat_messages
|
|
836
|
+
WHERE platform = ? AND account_id = ? AND message_id = ?`,
|
|
837
|
+
)
|
|
838
|
+
.run(input.platform, input.accountId, input.messageId);
|
|
839
|
+
return Number(result.changes);
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
|
|
805
843
|
startConnection(input: StartConnectionInput): number | null {
|
|
806
844
|
return this.write(() => {
|
|
807
845
|
const now = input.connectStartedAt ?? Date.now();
|
package/src/ws-client.ts
CHANGED
|
@@ -651,6 +651,10 @@ export class ClawChatClient extends EventEmitter {
|
|
|
651
651
|
if (env.event === EVENT.MESSAGE_ACK) return this.onAck(env as Envelope<MessageAckPayload>);
|
|
652
652
|
if (env.event === EVENT.MESSAGE_ERROR) return this.onMessageError(env as Envelope<MessageErrorPayload>);
|
|
653
653
|
if (isBusinessDispatchEvent(env.event)) this.emit("message", env);
|
|
654
|
+
// Deliberately NOT a business dispatch event: `message` feeds the turn
|
|
655
|
+
// pipeline, and a recall has no body to turn into a prompt. It is an
|
|
656
|
+
// instruction to delete, so it gets its own emitter and its own consumer.
|
|
657
|
+
if (env.event === EVENT.MESSAGE_RECALL) this.emit("message:recall", env);
|
|
654
658
|
if (env.event === EVENT.MESSAGE_CREATED) this.emit("message:created", env);
|
|
655
659
|
if (env.event === EVENT.MESSAGE_ADD) this.emit("message:add", env);
|
|
656
660
|
if (env.event === EVENT.MESSAGE_DONE) this.emit("message:done", env);
|