@clawling/clawchat-plugin-openclaw 2026.10.9-2 → 2026.10.9-4
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/README.md +1 -1
- package/dist/index.js +4 -0
- package/dist/src/clawchat-memory.js +6 -1
- package/dist/src/config.js +6 -0
- package/dist/src/host-tools-guard.js +165 -0
- package/dist/src/host-version.js +35 -0
- package/dist/src/memory-scope.js +166 -0
- package/dist/src/outbound.js +28 -2
- package/dist/src/owner-identity.js +29 -0
- package/dist/src/reply-dispatcher.js +26 -13
- package/dist/src/runtime.js +73 -12
- package/dist/src/storage.js +153 -15
- package/dist/src/terminal-send.js +69 -12
- package/dist/src/tools.js +324 -36
- package/dist/src/turn-boundary.js +68 -0
- package/index.ts +4 -0
- package/openclaw.plugin.json +4 -0
- package/package.json +3 -3
- package/src/clawchat-memory.ts +13 -0
- package/src/config.ts +11 -0
- package/src/host-tools-guard.ts +189 -0
- package/src/host-version.ts +35 -0
- package/src/memory-scope.ts +191 -0
- package/src/outbound.ts +39 -3
- package/src/owner-identity.ts +49 -0
- package/src/reply-dispatcher.ts +31 -13
- package/src/runtime.ts +81 -13
- package/src/storage.ts +176 -16
- package/src/terminal-send.ts +97 -13
- package/src/tools.ts +347 -40
- package/src/turn-boundary.ts +79 -0
package/src/outbound.ts
CHANGED
|
@@ -814,23 +814,33 @@ export interface SendReactionParams {
|
|
|
814
814
|
log?: LogSink;
|
|
815
815
|
}
|
|
816
816
|
|
|
817
|
+
/**
|
|
818
|
+
* Outcome of {@link sendOpenclawClawlingReaction}. `sent: false` means the
|
|
819
|
+
* frame never left the plugin; callers that report to the model must surface
|
|
820
|
+
* it as a failure, never as a reaction.
|
|
821
|
+
*/
|
|
822
|
+
export type SendReactionResult =
|
|
823
|
+
| { sent: true }
|
|
824
|
+
| { sent: false; reason: "invalid_chat_id" | "chat_rejected_by_server" };
|
|
825
|
+
|
|
817
826
|
/**
|
|
818
827
|
* Emit a Protocol-v2 `message.reaction`. Fire-and-forget: reactions are NOT
|
|
819
828
|
* ack-aligned (the server mints the slot-key message_id, so the ack-by-trace_id
|
|
820
829
|
* path used for message.send/reply does not apply). Matches the mobile client.
|
|
830
|
+
* Local refusals are returned, not thrown; a transport failure still throws.
|
|
821
831
|
*/
|
|
822
|
-
export function sendOpenclawClawlingReaction(params: SendReactionParams):
|
|
832
|
+
export function sendOpenclawClawlingReaction(params: SendReactionParams): SendReactionResult {
|
|
823
833
|
if (!isValidChatId(params.chatId)) {
|
|
824
834
|
params.log?.info?.(
|
|
825
835
|
`clawchat reaction blocked chat_id=${params.chatId} reason=invalid_chat_id`,
|
|
826
836
|
);
|
|
827
|
-
return;
|
|
837
|
+
return { sent: false, reason: "invalid_chat_id" };
|
|
828
838
|
}
|
|
829
839
|
if (isOutboundBlocked(params.client, params.chatId)) {
|
|
830
840
|
params.log?.info?.(
|
|
831
841
|
`clawchat reaction blocked chat_id=${params.chatId} reason=chat_rejected_by_server`,
|
|
832
842
|
);
|
|
833
|
-
return;
|
|
843
|
+
return { sent: false, reason: "chat_rejected_by_server" };
|
|
834
844
|
}
|
|
835
845
|
const payload: MessageReactionPayload = {
|
|
836
846
|
target_message_id: params.targetMessageId,
|
|
@@ -849,6 +859,7 @@ export function sendOpenclawClawlingReaction(params: SendReactionParams): void {
|
|
|
849
859
|
params.log?.info?.(
|
|
850
860
|
`clawchat reaction sent chat_id=${params.chatId} target=${params.targetMessageId} emoji=${params.emoji} removed=${params.removed}`,
|
|
851
861
|
);
|
|
862
|
+
return { sent: true };
|
|
852
863
|
}
|
|
853
864
|
|
|
854
865
|
type OutboundClaimStore = Pick<ClawChatStore, "claimMessageOnce" | "markMessageAcknowledged">;
|
|
@@ -875,6 +886,31 @@ function ulid(now: number = Date.now()): string {
|
|
|
875
886
|
return out.join("");
|
|
876
887
|
}
|
|
877
888
|
|
|
889
|
+
// Every message id shape a ClawChat message can carry. The server preserves a
|
|
890
|
+
// client-supplied id verbatim and never parses it, so this is deliberately a
|
|
891
|
+
// plausibility check, not the mint contract:
|
|
892
|
+
// - `msg-<ULID>`: clients (this plugin, mobile, hermes) and the server's own
|
|
893
|
+
// fallback mint; `msg_…` / short `msg-…` forms are kept for older ids;
|
|
894
|
+
// - `sys-<ULID>` / `sys-<request_id>`: server system injections;
|
|
895
|
+
// - a bare ULID: the server's user-message publisher;
|
|
896
|
+
// - `clawchat-plugin-openclaw-…`: this plugin's synthetic turn ids.
|
|
897
|
+
// UUIDs are not accepted: no ClawChat message id the model sees has that
|
|
898
|
+
// shape, while host transcript ids (e.g. from `sessions_search`) do.
|
|
899
|
+
const PREFIXED_MESSAGE_ID = /^(?:msg[-_]|sys-|clawchat-plugin-openclaw-)\S+$/i;
|
|
900
|
+
const BARE_ULID = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Whether `id` can be a real ClawChat message id. Rejects what a model makes
|
|
904
|
+
* up when it has no id (a label like `B`, `msg`, prose); the server only checks
|
|
905
|
+
* that a reaction target is non-empty, so such a value would "succeed" on no
|
|
906
|
+
* message. Callers trim first. Tools use this only as the fallback when the
|
|
907
|
+
* plugin store cannot answer whether it has seen the id in the chat.
|
|
908
|
+
*/
|
|
909
|
+
export function isPlausibleMessageId(id: unknown): id is string {
|
|
910
|
+
if (typeof id !== "string") return false;
|
|
911
|
+
return PREFIXED_MESSAGE_ID.test(id) || BARE_ULID.test(id);
|
|
912
|
+
}
|
|
913
|
+
|
|
878
914
|
/** Client-minted message id: `msg-` + ULID (protocol §7.6 format contract). */
|
|
879
915
|
export function mintMessageId(): string {
|
|
880
916
|
return `msg-${ulid()}`;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// The one definition of "who is the owner, and which chat is the owner's DM"
|
|
2
|
+
// shared by dispatch, the host-tool guard and memory scope. The SQLite
|
|
3
|
+
// activation row decides; the config `ownerUserId` mirror is only a fallback
|
|
4
|
+
// once a working store has proved there is no activation row at all. An
|
|
5
|
+
// unavailable store, a failed read, a cleared/rejected token or an incomplete
|
|
6
|
+
// row never falls back to the mirror — the caller sees no owner (fail closed).
|
|
7
|
+
|
|
8
|
+
export type ActivationIdentityLookup =
|
|
9
|
+
| { status: "unavailable" }
|
|
10
|
+
| { status: "absent" }
|
|
11
|
+
| {
|
|
12
|
+
status: "row";
|
|
13
|
+
row: { ownerUserId: string | null; conversationId: string | null; accessToken: string | null };
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Structural so focused fixtures can omit unrelated store methods. */
|
|
17
|
+
export type OwnerIdentityStore = {
|
|
18
|
+
lookupActivationIdentity?(input: { platform: string; accountId: string }): ActivationIdentityLookup;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type OwnerIdentity = { ownerUserId: string | null; conversationId: string | null };
|
|
22
|
+
|
|
23
|
+
export function resolveOwnerIdentity(
|
|
24
|
+
store: OwnerIdentityStore | null | undefined,
|
|
25
|
+
accountId: string,
|
|
26
|
+
configOwnerUserId?: string | null,
|
|
27
|
+
warn?: (failure: string) => void,
|
|
28
|
+
rejectedActivationToken?: string,
|
|
29
|
+
): OwnerIdentity {
|
|
30
|
+
const denied: OwnerIdentity = { ownerUserId: null, conversationId: null };
|
|
31
|
+
try {
|
|
32
|
+
const lookup = store?.lookupActivationIdentity?.({ platform: "openclaw", accountId });
|
|
33
|
+
if (!lookup || lookup.status === "unavailable") {
|
|
34
|
+
warn?.("owner-identity");
|
|
35
|
+
return denied;
|
|
36
|
+
}
|
|
37
|
+
if (lookup.status === "absent") {
|
|
38
|
+
return { ownerUserId: configOwnerUserId?.trim() || null, conversationId: null };
|
|
39
|
+
}
|
|
40
|
+
const { row } = lookup;
|
|
41
|
+
// Mirror the runtime overlay: never trust the activation rejected during this lifecycle.
|
|
42
|
+
if (!row.accessToken || row.accessToken === rejectedActivationToken) return denied;
|
|
43
|
+
const ownerUserId = row.ownerUserId?.trim() || null;
|
|
44
|
+
return { ownerUserId, conversationId: ownerUserId ? row.conversationId?.trim() || null : null };
|
|
45
|
+
} catch {
|
|
46
|
+
warn?.("owner-identity");
|
|
47
|
+
return denied;
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -88,7 +88,13 @@ export interface ReplyDispatcherOptions {
|
|
|
88
88
|
* framework error final; null/absent means English.
|
|
89
89
|
*/
|
|
90
90
|
resolveOwnerLocale?: () => string | null;
|
|
91
|
-
|
|
91
|
+
/**
|
|
92
|
+
* True when the turn was not started by the owner (any group turn, any
|
|
93
|
+
* non-owner direct chat). Reply media is then limited to http(s) URLs; local
|
|
94
|
+
* paths are never loaded. Legacy standalone callers default to owner behavior.
|
|
95
|
+
*/
|
|
96
|
+
nonOwnerTurn?: boolean;
|
|
97
|
+
log?: { info?: (m: string) => void; warn?: (m: string) => void; error?: (m: string) => void };
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
type TypedReplyDispatcherResult = ReturnType<
|
|
@@ -435,6 +441,19 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
435
441
|
log,
|
|
436
442
|
} = options;
|
|
437
443
|
const isGroupTarget = target.chatType === "group";
|
|
444
|
+
let warnedLocalReplyMedia = false;
|
|
445
|
+
// Non-owner turns may only send remote (http/https) media. Local sources are
|
|
446
|
+
// dropped here, before any load, because the host's local media roots include
|
|
447
|
+
// the agent workspace. Warns once per dispatcher and never names the source.
|
|
448
|
+
function filterReplyMedia(urls: string[]): string[] {
|
|
449
|
+
if (!options.nonOwnerTurn) return urls;
|
|
450
|
+
const remote = urls.filter((source) => /^https?:\/\//i.test(source.trim()));
|
|
451
|
+
if (remote.length !== urls.length && !warnedLocalReplyMedia) {
|
|
452
|
+
warnedLocalReplyMedia = true;
|
|
453
|
+
log?.warn?.("clawchat-plugin-openclaw: non-owner reply media dropped; only http(s) URLs are allowed");
|
|
454
|
+
}
|
|
455
|
+
return remote;
|
|
456
|
+
}
|
|
438
457
|
const outputVisibility = effectiveOutputVisibility(account, target.chatId, target.chatType);
|
|
439
458
|
const splitFullOutput = outputVisibility === "full";
|
|
440
459
|
const splitNormalBlockOutput = outputVisibility === "normal";
|
|
@@ -442,11 +461,10 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
442
461
|
// owner's usr_… id: chat_id is resolved by msghub through member-backend, and
|
|
443
462
|
// a usr_… comes back "invalid conversation id" — the frame is dropped with no
|
|
444
463
|
// negative ack to this client, so the owner silently never gets the approval
|
|
445
|
-
// request.
|
|
446
|
-
//
|
|
464
|
+
// request. The resolver is the shared strict owner-identity lookup
|
|
465
|
+
// (src/owner-identity.ts): it yields a conversation only for an activation
|
|
466
|
+
// row with an owner, so the config `ownerUserId` mirror is not consulted.
|
|
447
467
|
const ownerDirectTarget = (): { chatId: string; chatType: "direct" } | null => {
|
|
448
|
-
const ownerUserId = account.ownerUserId?.trim();
|
|
449
|
-
if (!ownerUserId) return null;
|
|
450
468
|
const conversationId = resolveOwnerConversationId?.()?.trim();
|
|
451
469
|
return conversationId ? { chatId: conversationId, chatType: "direct" as const } : null;
|
|
452
470
|
};
|
|
@@ -462,7 +480,8 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
462
480
|
});
|
|
463
481
|
};
|
|
464
482
|
|
|
465
|
-
async function uploadMediaUrls(
|
|
483
|
+
async function uploadMediaUrls(inputUrls: string[]): Promise<ClawlingMediaFragment[]> {
|
|
484
|
+
const urls = filterReplyMedia(inputUrls);
|
|
466
485
|
if (urls.length === 0) return [];
|
|
467
486
|
const apiClient = buildApiClient();
|
|
468
487
|
if (!apiClient) {
|
|
@@ -720,11 +739,8 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
720
739
|
): Promise<void> => {
|
|
721
740
|
const ownerTarget = ownerDirectTarget();
|
|
722
741
|
if (!ownerTarget) {
|
|
723
|
-
const reason = account.ownerUserId?.trim()
|
|
724
|
-
? "missing_owner_conversation"
|
|
725
|
-
: "missing_owner_user_id";
|
|
726
742
|
log?.error?.(
|
|
727
|
-
`[${account.accountId}] clawchat-plugin-openclaw group owner attention suppressed reason
|
|
743
|
+
`[${account.accountId}] clawchat-plugin-openclaw group owner attention suppressed reason=missing_owner_conversation group=${target.chatId}`,
|
|
728
744
|
);
|
|
729
745
|
return;
|
|
730
746
|
}
|
|
@@ -917,7 +933,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
917
933
|
if (consumeTerminalSend(info?.kind ?? "unknown")) return;
|
|
918
934
|
const richFragment = buildRichInteractionFragment(payload);
|
|
919
935
|
const text = richFragment && account.richInteractions ? "" : resolvePayloadText(payload);
|
|
920
|
-
const urls = resolveOutboundMediaUrls(payload).filter(Boolean);
|
|
936
|
+
const urls = filterReplyMedia(resolveOutboundMediaUrls(payload).filter(Boolean));
|
|
921
937
|
log?.info?.(
|
|
922
938
|
`[${account.accountId}] clawchat-plugin-openclaw deliver kind=${info?.kind ?? "unknown"} text_len=${text.length} media_urls=${urls.length} reasoning=${payload.isReasoning === true}`,
|
|
923
939
|
);
|
|
@@ -1021,6 +1037,8 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
1021
1037
|
log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw final skipped: already delivered at a message boundary`);
|
|
1022
1038
|
return;
|
|
1023
1039
|
}
|
|
1040
|
+
// A non-owner final whose only content was dropped local media sends nothing.
|
|
1041
|
+
if (options.nonOwnerTurn && !finalText.trim() && !richFragment && finalUrls.length === 0) return;
|
|
1024
1042
|
const mediaFragments = await uploadMediaUrls(finalUrls);
|
|
1025
1043
|
const result = await sendStatic(
|
|
1026
1044
|
finalText,
|
|
@@ -1097,7 +1115,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
1097
1115
|
? async (payload: ReplyPayload) => {
|
|
1098
1116
|
if (consumeTerminalSend("reasoning")) return;
|
|
1099
1117
|
const text = resolvePayloadText(payload);
|
|
1100
|
-
await emitFullRuntimeText("reasoning", text, resolveOutboundMediaUrls(payload).filter(Boolean));
|
|
1118
|
+
await emitFullRuntimeText("reasoning", text, filterReplyMedia(resolveOutboundMediaUrls(payload).filter(Boolean)));
|
|
1101
1119
|
const trimmed = text.trim();
|
|
1102
1120
|
if (trimmed) reasoningText = reasoningText ? `${reasoningText}\n${trimmed}` : trimmed;
|
|
1103
1121
|
}
|
|
@@ -1120,7 +1138,7 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
|
|
|
1120
1138
|
if (consumeTerminalSend("tool-result")) return;
|
|
1121
1139
|
const text = resolvePayloadText(payload);
|
|
1122
1140
|
if (isDefaultToolResultText(text)) return;
|
|
1123
|
-
await emitFullRuntimeText("tool result", text, resolveOutboundMediaUrls(payload).filter(Boolean));
|
|
1141
|
+
await emitFullRuntimeText("tool result", text, filterReplyMedia(resolveOutboundMediaUrls(payload).filter(Boolean)));
|
|
1124
1142
|
}
|
|
1125
1143
|
: undefined,
|
|
1126
1144
|
onItemEvent: splitFullOutput
|
package/src/runtime.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { applyDmRecallDefault } from "./dm-recall-default.ts";
|
|
2
|
+
import { HOST_FLOOR, HOST_TOOL_SEARCH_DEFAULT_ON, hostAtLeast, hostToolSearchEnabled } from "./host-version.ts";
|
|
2
3
|
import {
|
|
3
4
|
AckTimeoutError,
|
|
4
5
|
AuthError,
|
|
@@ -86,7 +87,9 @@ import {
|
|
|
86
87
|
type ClawChatStore,
|
|
87
88
|
type MessageRow,
|
|
88
89
|
} from "./storage.ts";
|
|
89
|
-
import { getClawChatGroupPrompt, getClawChatUserPrompt } from "./plugin-prompts.ts";
|
|
90
|
+
import { getClawChatGroupPrompt, getClawChatPlatformPrompt, getClawChatUserPrompt } from "./plugin-prompts.ts";
|
|
91
|
+
import { classifyChat, NON_OWNER_TOOL_ALLOWLIST } from "./turn-boundary.ts";
|
|
92
|
+
import { resolveOwnerIdentity } from "./owner-identity.ts";
|
|
90
93
|
import {
|
|
91
94
|
loadClawChatPromptMetadata,
|
|
92
95
|
type ClawChatGroupParticipantPrompt,
|
|
@@ -111,7 +114,8 @@ import { PermissionCache } from "./permissions.ts";
|
|
|
111
114
|
import { buildAwarenessNoteEnvelope, buildMomentCommentNoteEnvelope } from "./awareness-note-turn.ts";
|
|
112
115
|
import { handlePermissionResult } from "./permission-result.ts";
|
|
113
116
|
|
|
114
|
-
type Log = { debug?: (m: string) => void; info?: (m: string) => void;
|
|
117
|
+
type Log = { debug?: (m: string) => void; info?: (m: string) => void;
|
|
118
|
+
warn?: (m: string) => void; error?: (m: string) => void };
|
|
115
119
|
type ChannelContextBuilder = (params: Record<string, unknown>) => unknown;
|
|
116
120
|
type CompatibleChannelRuntime = PluginRuntime["channel"] & {
|
|
117
121
|
inbound?: { buildContext?: ChannelContextBuilder };
|
|
@@ -140,6 +144,7 @@ type RuntimeConnectionStore = Pick<
|
|
|
140
144
|
| "releaseActivationBootstrapClaim"
|
|
141
145
|
| "markActivationBootstrapSent"
|
|
142
146
|
| "getActivationConversation"
|
|
147
|
+
| "lookupActivationIdentity"
|
|
143
148
|
| "getLastResolvedDeviceId"
|
|
144
149
|
| "listRecentGroupMessages"
|
|
145
150
|
| "recallMessage"
|
|
@@ -585,6 +590,8 @@ export interface StartGatewayParams {
|
|
|
585
590
|
setStatus: (next: ChannelAccountSnapshot) => void;
|
|
586
591
|
getStatus: () => ChannelAccountSnapshot;
|
|
587
592
|
log?: Log;
|
|
593
|
+
/** Internal — set on recursive re-entry so the approve-unsupported warning is issued once per lifecycle. */
|
|
594
|
+
boundaryWarningIssued?: boolean;
|
|
588
595
|
/** Test hook only. */
|
|
589
596
|
store?: RuntimeConnectionStore | null;
|
|
590
597
|
/** Test hook only. */
|
|
@@ -1057,6 +1064,9 @@ async function waitForActivationCredentials(params: {
|
|
|
1057
1064
|
return null;
|
|
1058
1065
|
}
|
|
1059
1066
|
|
|
1067
|
+
let warnedBelowHostFloor = false;
|
|
1068
|
+
let loggedHostToolSearch = false;
|
|
1069
|
+
|
|
1060
1070
|
export async function startOpenclawClawlingGateway(params: StartGatewayParams): Promise<void> {
|
|
1061
1071
|
const { cfg, abortSignal, setStatus, getStatus, log } = params;
|
|
1062
1072
|
let account = params.account;
|
|
@@ -1064,6 +1074,21 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1064
1074
|
const runtime = getOpenclawClawlingRuntime();
|
|
1065
1075
|
const accountId = account.accountId;
|
|
1066
1076
|
const store = resolveConnectionStore(params, runtime);
|
|
1077
|
+
if (!warnedBelowHostFloor && !hostAtLeast(runtime.version, HOST_FLOOR)) {
|
|
1078
|
+
warnedBelowHostFloor = true;
|
|
1079
|
+
log?.warn?.(`clawchat-plugin-openclaw: host ${runtime.version ?? "unknown"} is below ${HOST_FLOOR}; turns your owner did not start keep the host tools in their schema and rely on the before_tool_call block`);
|
|
1080
|
+
}
|
|
1081
|
+
if (
|
|
1082
|
+
!loggedHostToolSearch &&
|
|
1083
|
+
hostAtLeast(runtime.version, HOST_TOOL_SEARCH_DEFAULT_ON) &&
|
|
1084
|
+
hostToolSearchEnabled(cfg)
|
|
1085
|
+
) {
|
|
1086
|
+
loggedHostToolSearch = true;
|
|
1087
|
+
log?.info?.("clawchat-plugin-openclaw: host tool search is on; turns your owner did not start can still use only the allowlisted tools (set tools.toolSearch.enabled=false to turn tool search off; this affects every turn)");
|
|
1088
|
+
}
|
|
1089
|
+
if (!params.boundaryWarningIssued && account.nonOwnerHostTools === "approve") {
|
|
1090
|
+
log?.warn?.("clawchat-plugin-openclaw: non-owner-host-tools approve is not supported yet; using off");
|
|
1091
|
+
}
|
|
1067
1092
|
log?.info?.(
|
|
1068
1093
|
`[${accountId}] clawchat-plugin-openclaw runtime start entered configured=${account.configured} enabled=${account.enabled} hasToken=${Boolean(account.token)} hasUserId=${Boolean(account.userId)} hasOwnerUserId=${Boolean(account.ownerUserId)} websocketUrl=${account.websocketUrl || "(empty)"}`,
|
|
1069
1094
|
);
|
|
@@ -1897,6 +1922,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1897
1922
|
if (abortSignal.aborted) return;
|
|
1898
1923
|
void startOpenclawClawlingGateway({
|
|
1899
1924
|
...params,
|
|
1925
|
+
boundaryWarningIssued: true,
|
|
1900
1926
|
account: { ...params.account },
|
|
1901
1927
|
// Carry the live refresh token forward (a rotation earlier this lifecycle
|
|
1902
1928
|
// leaves the in-memory `cfg` stale; see refreshTokenOverride).
|
|
@@ -1944,6 +1970,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
1944
1970
|
// agent must NOT re-read it — thread the live refresh token forward instead.
|
|
1945
1971
|
await startOpenclawClawlingGateway({
|
|
1946
1972
|
...params,
|
|
1973
|
+
boundaryWarningIssued: true,
|
|
1947
1974
|
account: {
|
|
1948
1975
|
...params.account,
|
|
1949
1976
|
configured: true,
|
|
@@ -2552,14 +2579,24 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
2552
2579
|
// "invalid conversation id" and msghub never acks the send. Proactive owner
|
|
2553
2580
|
// messages must be addressed to the direct conversation recorded at
|
|
2554
2581
|
// activation (parity with the hermes adapter's _owner_direct_chat_id).
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2582
|
+
// One warning per failure class for this lifecycle; never logs ids or SQL.
|
|
2583
|
+
const ownerIdentityWarnings = new Set<string>();
|
|
2584
|
+
const warnOwnerIdentity = (failure: string) => {
|
|
2585
|
+
if (ownerIdentityWarnings.has(failure)) return;
|
|
2586
|
+
ownerIdentityWarnings.add(failure);
|
|
2587
|
+
try {
|
|
2588
|
+
log?.warn?.(`clawchat-plugin-openclaw: ${failure}; owner identity unavailable`);
|
|
2589
|
+
} catch {
|
|
2590
|
+
// Logging cannot change the (fail-closed) answer.
|
|
2591
|
+
}
|
|
2562
2592
|
};
|
|
2593
|
+
const resolveOwnerDirectConversationId = (): string | null =>
|
|
2594
|
+
// Shared strict resolver (src/owner-identity.ts): the SQLite activation
|
|
2595
|
+
// row decides; the config `ownerUserId` mirror may be stale or absent and
|
|
2596
|
+
// can neither hide nor invent the owner's direct chat. Unavailable storage
|
|
2597
|
+
// or a cleared/rejected token yields no owner DM (fail closed).
|
|
2598
|
+
resolveOwnerIdentity(store, accountId, account.ownerUserId, warnOwnerIdentity,
|
|
2599
|
+
params.rejectedActivationToken).conversationId;
|
|
2563
2600
|
// R3: the owner's direct conversation is never gated by the dead-chat send
|
|
2564
2601
|
// guard. If it were, the very message explaining why the agent went dark
|
|
2565
2602
|
// (skill-update notices, auth/logout notices, group owner-attention
|
|
@@ -3338,8 +3375,28 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
3338
3375
|
}
|
|
3339
3376
|
turn = await withCachedGroupSenderNames(turn, memoryRoot);
|
|
3340
3377
|
const conversationTarget = `${CHANNEL_ID}:${formatConversationSubject(turn.peer)}`;
|
|
3341
|
-
const turnPrompt = await buildPromptForTurn(turn, memoryRoot);
|
|
3342
3378
|
const senderIsOwner = turn.senderIsOwner === true || turn.senderId === account.ownerUserId;
|
|
3379
|
+
// Owner turn <=> the owner's activation direct chat. Resolved per turn;
|
|
3380
|
+
// any failure leaves it null, which classifies as non-owner (fail closed).
|
|
3381
|
+
let ownerConversationId: string | null = null;
|
|
3382
|
+
try {
|
|
3383
|
+
ownerConversationId = resolveOwnerDirectConversationId();
|
|
3384
|
+
} catch {
|
|
3385
|
+
ownerConversationId = null;
|
|
3386
|
+
}
|
|
3387
|
+
const turnClass = classifyChat({
|
|
3388
|
+
kind: turn.peer.kind,
|
|
3389
|
+
chatId: turn.peer.id,
|
|
3390
|
+
ownerDirectConversationId: ownerConversationId,
|
|
3391
|
+
});
|
|
3392
|
+
const narrowHostTools = turnClass === "non-owner";
|
|
3393
|
+
const baseTurnPrompt = await buildPromptForTurn(turn, memoryRoot);
|
|
3394
|
+
const nonOwnerMediaGuidance =
|
|
3395
|
+
"To send an image or file here, include it in your final reply as an http(s) URL; local files cannot be sent from this chat.";
|
|
3396
|
+
const turnPrompt = [
|
|
3397
|
+
...(narrowHostTools ? [getClawChatPlatformPrompt(), nonOwnerMediaGuidance] : []),
|
|
3398
|
+
baseTurnPrompt,
|
|
3399
|
+
].filter(Boolean).join("\n\n");
|
|
3343
3400
|
const ctxPayload = resolveChannelContextBuilder(rt)({
|
|
3344
3401
|
channel: CHANNEL_ID,
|
|
3345
3402
|
accountId: route.accountId ?? accountId,
|
|
@@ -3385,6 +3442,8 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
3385
3442
|
...(isOpenClawTextSlashCommand(turn.rawBody, cfg) && senderIsOwner
|
|
3386
3443
|
? { commands: { authorized: true, source: "text" } }
|
|
3387
3444
|
: {}),
|
|
3445
|
+
// toolPolicy: host 2026.8.1+ (floor)
|
|
3446
|
+
...(narrowHostTools ? { toolPolicy: { allow: [...NON_OWNER_TOOL_ALLOWLIST] } } : {}),
|
|
3388
3447
|
},
|
|
3389
3448
|
...(memoryRoot ? { extra: { memoryRoot } } : {}),
|
|
3390
3449
|
// Deliver the rendered ClawChat per-turn prompt (owner agent_behavior,
|
|
@@ -3440,7 +3499,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
3440
3499
|
if (stagedDirectPrompt) {
|
|
3441
3500
|
stageClawChatPromptInjection({
|
|
3442
3501
|
sessionKey: resolvedSessionKey,
|
|
3443
|
-
prompt:
|
|
3502
|
+
prompt: baseTurnPrompt,
|
|
3444
3503
|
});
|
|
3445
3504
|
}
|
|
3446
3505
|
|
|
@@ -3501,6 +3560,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
3501
3560
|
// conversation, not `ownerUserId` (a usr_… chat_id is rejected by
|
|
3502
3561
|
// member-backend). Passed as a thunk so it is resolved at send time.
|
|
3503
3562
|
resolveOwnerConversationId: resolveOwnerDirectConversationId,
|
|
3563
|
+
nonOwnerTurn: narrowHostTools,
|
|
3504
3564
|
log,
|
|
3505
3565
|
});
|
|
3506
3566
|
|
|
@@ -3514,8 +3574,15 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
3514
3574
|
dispatcher,
|
|
3515
3575
|
onSettled: () => markDispatchIdle(),
|
|
3516
3576
|
run: () =>
|
|
3517
|
-
runWithTerminalClawChatSendScope(terminalSendScopeId, () =>
|
|
3518
|
-
rt.reply.dispatchReplyFromConfig({
|
|
3577
|
+
runWithTerminalClawChatSendScope({ scopeId: terminalSendScopeId, sessionKey: resolvedSessionKey }, () =>
|
|
3578
|
+
rt.reply.dispatchReplyFromConfig({
|
|
3579
|
+
ctx: ctxPayload,
|
|
3580
|
+
cfg: dispatchCfg,
|
|
3581
|
+
dispatcher,
|
|
3582
|
+
replyOptions: narrowHostTools
|
|
3583
|
+
? { ...replyOptions, toolsAllow: [...NON_OWNER_TOOL_ALLOWLIST] }
|
|
3584
|
+
: replyOptions,
|
|
3585
|
+
}),
|
|
3519
3586
|
),
|
|
3520
3587
|
});
|
|
3521
3588
|
const counts = (dispatchResult as { counts?: Record<string, number> } | undefined)?.counts ?? {};
|
|
@@ -4101,6 +4168,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
4101
4168
|
if (activationAccount.source === "sqlite" && !abortSignal.aborted) {
|
|
4102
4169
|
await startOpenclawClawlingGateway({
|
|
4103
4170
|
...params,
|
|
4171
|
+
boundaryWarningIssued: true,
|
|
4104
4172
|
account: {
|
|
4105
4173
|
...params.account,
|
|
4106
4174
|
configured: false,
|