@clawling/clawchat-plugin-openclaw 2026.10.8-2 → 2026.10.9-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.
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Turn off OpenClaw's cross-conversation recall for the agent ClawChat routes
3
+ * to, once, unless the owner already decided.
4
+ *
5
+ * OpenClaw >= 2026.8.1 has `memory.search.rememberAcrossConversations`. When it
6
+ * is not written anywhere, OpenClaw turns it on unless `session.dmScope` or a
7
+ * binding isolates DMs (`resolveRememberAcrossConversations`,
8
+ * packages/memory-host-sdk/src/host/config-utils.ts). The plugin isolates
9
+ * ClawChat DMs only in memory (`withClawChatSessionScope`), which OpenClaw
10
+ * cannot see, so recall stays on and a friend's DM can recall the owner's.
11
+ * Active Memory reads the live gateway config each turn, so a per-turn
12
+ * override does not reach it: the setting has to be in the config file.
13
+ *
14
+ * Where it is read (2026.8.1 / 2026.9.x): the agent's `agents.entries.<id>`
15
+ * or, when the roster is the legacy array, its `agents.list[]` entry (matched
16
+ * by `id`); then the global `memory.search`. With no roster in the file there
17
+ * is one implicit agent, so the global layer IS that agent's.
18
+ *
19
+ * Rules (relay openclaw-dm-recall, 10-08):
20
+ * - only on hosts >= 2026.8.1 (older ones have neither the key nor the recall);
21
+ * - only when neither layer sets it; an explicit `true` is the owner's choice:
22
+ * kept, warned once; an explicit `false` is kept silently;
23
+ * - only once per agent: a marker in the plugin's state dir records it, so
24
+ * whatever the owner changes later is never touched again.
25
+ */
26
+ import fs from "node:fs";
27
+ import path from "node:path";
28
+ export const DM_RECALL_MARKER_FILENAME = "clawchat-dm-recall-default.json";
29
+ export const DM_RECALL_MIN_HOST = [2026, 8, 1];
30
+ function isRec(value) {
31
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
32
+ }
33
+ function normalizeId(id) {
34
+ return typeof id === "string" ? id.trim().toLowerCase() : "";
35
+ }
36
+ /** True for a release host >= 2026.8.1. Pre-releases and unknown strings are false. */
37
+ export function hostHasRememberAcrossConversations(version) {
38
+ const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec((version ?? "").trim());
39
+ if (!m)
40
+ return false;
41
+ const parts = [Number(m[1]), Number(m[2]), Number(m[3])];
42
+ for (let i = 0; i < 3; i += 1) {
43
+ if (parts[i] !== DM_RECALL_MIN_HOST[i])
44
+ return parts[i] > DM_RECALL_MIN_HOST[i];
45
+ }
46
+ return true;
47
+ }
48
+ function findAgentSlot(cfg, agentId) {
49
+ const agents = isRec(cfg.agents) ? cfg.agents : undefined;
50
+ const id = normalizeId(agentId);
51
+ if (agents && Object.hasOwn(agents, "entries") && agents.entries !== undefined) {
52
+ if (!isRec(agents.entries))
53
+ return { kind: "missing" };
54
+ for (const key of Object.keys(agents.entries)) {
55
+ const entry = agents.entries[key];
56
+ if (normalizeId(key) === id && isRec(entry))
57
+ return { kind: "entries", key, entry };
58
+ }
59
+ return { kind: "missing" };
60
+ }
61
+ if (agents && Object.hasOwn(agents, "list") && agents.list !== undefined) {
62
+ if (!Array.isArray(agents.list))
63
+ return { kind: "missing" };
64
+ const index = agents.list.findIndex((e) => isRec(e) && normalizeId(e.id) === id);
65
+ return index >= 0 ? { kind: "list", index, entry: agents.list[index] } : { kind: "missing" };
66
+ }
67
+ return { kind: "no-roster" };
68
+ }
69
+ function readFlag(holder) {
70
+ if (!isRec(holder) || !isRec(holder.memory) || !isRec(holder.memory.search))
71
+ return undefined;
72
+ const v = holder.memory.search.rememberAcrossConversations;
73
+ return typeof v === "boolean" ? v : undefined;
74
+ }
75
+ /** The value written at each layer, `undefined` where it is not written. */
76
+ export function readRememberAcrossConversations(cfg, agentId) {
77
+ const slot = findAgentSlot(cfg, agentId);
78
+ const agent = slot.kind === "entries" || slot.kind === "list" ? readFlag(slot.entry) : undefined;
79
+ return { agent, global: readFlag(cfg) };
80
+ }
81
+ function setFlagFalse(holder) {
82
+ const memory = isRec(holder.memory) ? holder.memory : {};
83
+ const search = isRec(memory.search) ? memory.search : {};
84
+ holder.memory = { ...memory, search: { ...search, rememberAcrossConversations: false } };
85
+ }
86
+ /**
87
+ * Write `rememberAcrossConversations: false` at the agent's own layer of
88
+ * `cfg` (mutated in place). Returns the dotted path written, or `null` when a
89
+ * roster exists but does not list the agent (nothing written).
90
+ */
91
+ export function setAgentRememberAcrossConversationsFalse(cfg, agentId) {
92
+ const slot = findAgentSlot(cfg, agentId);
93
+ if (slot.kind === "entries") {
94
+ setFlagFalse(slot.entry);
95
+ return `agents.entries.${slot.key}.memory.search`;
96
+ }
97
+ if (slot.kind === "list") {
98
+ setFlagFalse(slot.entry);
99
+ return `agents.list[${slot.index}].memory.search`;
100
+ }
101
+ if (slot.kind === "no-roster") {
102
+ setFlagFalse(cfg);
103
+ return "memory.search";
104
+ }
105
+ return null;
106
+ }
107
+ function readMarker(file) {
108
+ try {
109
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
110
+ return isRec(parsed) ? parsed : {};
111
+ }
112
+ catch {
113
+ return {};
114
+ }
115
+ }
116
+ function writeMarker(file, agentId, outcome) {
117
+ const marker = readMarker(file);
118
+ const agents = isRec(marker.agents) ? marker.agents : {};
119
+ agents[normalizeId(agentId)] = { outcome, at: new Date().toISOString() };
120
+ fs.mkdirSync(path.dirname(file), { recursive: true });
121
+ fs.writeFileSync(file, `${JSON.stringify({ ...marker, agents }, null, 2)}\n`);
122
+ }
123
+ /** Apply the one-time default. Never throws. */
124
+ export async function applyDmRecallDefault(params) {
125
+ const { agentId, log } = params;
126
+ const markerFile = path.join(params.stateDir, DM_RECALL_MARKER_FILENAME);
127
+ try {
128
+ if (readMarker(markerFile).agents?.[normalizeId(agentId)])
129
+ return "already-handled";
130
+ if (!hostHasRememberAcrossConversations(params.hostVersion))
131
+ return "unsupported-host";
132
+ const cfg = params.readConfig();
133
+ const current = readRememberAcrossConversations(isRec(cfg) ? cfg : {}, agentId);
134
+ const explicit = current.agent ?? current.global;
135
+ if (explicit === true) {
136
+ log?.warn?.(`clawchat-plugin-openclaw: memory.search.rememberAcrossConversations is true for agent '${agentId}', so a ` +
137
+ "friend's ClawChat direct chat can recall what the owner said in theirs. Kept as set. To turn it off: " +
138
+ `openclaw config set memory.search.rememberAcrossConversations false (or set it to false on agent '${agentId}'), then restart the gateway.`);
139
+ writeMarker(markerFile, agentId, "kept-true");
140
+ return "kept-true";
141
+ }
142
+ if (explicit === false) {
143
+ writeMarker(markerFile, agentId, "kept-false");
144
+ return "kept-false";
145
+ }
146
+ if (findAgentSlot(isRec(cfg) ? cfg : {}, agentId).kind === "missing") {
147
+ log?.info?.(`clawchat-plugin-openclaw: agent '${agentId}' is not in the agents roster; rememberAcrossConversations left unset`);
148
+ return "agent-not-in-roster";
149
+ }
150
+ let written = null;
151
+ let raced;
152
+ await params.mutateConfigFile({
153
+ afterWrite: { mode: "none", reason: "clawchat-plugin-openclaw: no recall across ClawChat direct chats" },
154
+ mutate(draft) {
155
+ // Re-check on the locked, current file: someone may have set it meanwhile.
156
+ const now = readRememberAcrossConversations(draft, agentId);
157
+ raced = now.agent ?? now.global;
158
+ if (raced !== undefined)
159
+ return;
160
+ written = setAgentRememberAcrossConversationsFalse(draft, agentId);
161
+ },
162
+ });
163
+ if (written === null) {
164
+ if (raced === undefined)
165
+ return "agent-not-in-roster";
166
+ const outcome = raced ? "kept-true" : "kept-false";
167
+ writeMarker(markerFile, agentId, outcome);
168
+ return outcome;
169
+ }
170
+ writeMarker(markerFile, agentId, "wrote-false");
171
+ log?.info?.(`clawchat-plugin-openclaw: set ${written}.rememberAcrossConversations=false for agent '${agentId}' ` +
172
+ "(ClawChat direct chats with different people no longer recall each other)");
173
+ return "wrote-false";
174
+ }
175
+ catch (err) {
176
+ log?.error?.(`clawchat-plugin-openclaw: could not apply the rememberAcrossConversations default for agent '${agentId}': ${err instanceof Error ? err.message : String(err)}`);
177
+ return "write-failed";
178
+ }
179
+ }
@@ -1,3 +1,4 @@
1
+ import { applyDmRecallDefault } from "./dm-recall-default.js";
1
2
  import { AckTimeoutError, AuthError, ProtocolError, StateError, TransportError, EVENT, recallTargetMessageId, } from "./protocol-types.js";
2
3
  import { waitUntilAbort } from "openclaw/plugin-sdk/channel-lifecycle";
3
4
  import { hasControlCommand } from "openclaw/plugin-sdk/command-detection";
@@ -937,6 +938,48 @@ export async function startOpenclawClawlingGateway(params) {
937
938
  ? runtimeConfig.mutateConfigFile
938
939
  : undefined;
939
940
  };
941
+ // OpenClaw >= 2026.8.1 recalls across a single agent's DMs unless DM isolation
942
+ // is visible in the gateway config; ours is in-memory only. Write the agent's
943
+ // `rememberAcrossConversations: false` once, unless the owner set it
944
+ // (dm-recall-default.ts). Fire-and-forget: never blocks or fails startup.
945
+ void (async () => {
946
+ const stateDir = runtime.state?.resolveStateDir?.();
947
+ const mutateConfigFile = resolveMutateConfigFile();
948
+ const readConfig = runtime.config?.current;
949
+ if (!stateDir || !mutateConfigFile || typeof readConfig !== "function")
950
+ return;
951
+ let agentId;
952
+ try {
953
+ agentId = runtime.channel.routing.resolveAgentRoute({
954
+ cfg: withClawChatSessionScope(cfg),
955
+ channel: CHANNEL_ID,
956
+ accountId,
957
+ peer: { kind: "direct", id: account.ownerUserId || "owner" },
958
+ }).agentId;
959
+ }
960
+ catch (err) {
961
+ log?.info?.(`[${accountId}] clawchat-plugin-openclaw rememberAcrossConversations default skipped: agent route unavailable (${err instanceof Error ? err.message : String(err)})`);
962
+ return;
963
+ }
964
+ await applyDmRecallDefault({
965
+ agentId,
966
+ hostVersion: runtime.version,
967
+ stateDir,
968
+ readConfig: () => readConfig.call(runtime.config),
969
+ mutateConfigFile: mutateConfigFile,
970
+ log: {
971
+ info: (m) => log?.info?.(`[${accountId}] ${m}`),
972
+ warn: (m) => {
973
+ const warn = log?.warn;
974
+ if (warn)
975
+ warn(`[${accountId}] ${m}`);
976
+ else
977
+ log?.info?.(`[${accountId}] WARNING ${m}`);
978
+ },
979
+ error: (m) => log?.error?.(`[${accountId}] ${m}`),
980
+ },
981
+ });
982
+ })();
940
983
  // §0/§C.1 — write the account slot's `token`/`refreshToken` keys. `tokens=null`
941
984
  // blanks them (auto-logout); otherwise persists the rotated pair.
942
985
  const persistConfigTokens = async (tokens) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawling/clawchat-plugin-openclaw",
3
- "version": "2026.10.8-2",
3
+ "version": "2026.10.9-1",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "license": "MIT",
6
6
  "author": "CLAWLING PTE. LTD.",
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Turn off OpenClaw's cross-conversation recall for the agent ClawChat routes
3
+ * to, once, unless the owner already decided.
4
+ *
5
+ * OpenClaw >= 2026.8.1 has `memory.search.rememberAcrossConversations`. When it
6
+ * is not written anywhere, OpenClaw turns it on unless `session.dmScope` or a
7
+ * binding isolates DMs (`resolveRememberAcrossConversations`,
8
+ * packages/memory-host-sdk/src/host/config-utils.ts). The plugin isolates
9
+ * ClawChat DMs only in memory (`withClawChatSessionScope`), which OpenClaw
10
+ * cannot see, so recall stays on and a friend's DM can recall the owner's.
11
+ * Active Memory reads the live gateway config each turn, so a per-turn
12
+ * override does not reach it: the setting has to be in the config file.
13
+ *
14
+ * Where it is read (2026.8.1 / 2026.9.x): the agent's `agents.entries.<id>`
15
+ * or, when the roster is the legacy array, its `agents.list[]` entry (matched
16
+ * by `id`); then the global `memory.search`. With no roster in the file there
17
+ * is one implicit agent, so the global layer IS that agent's.
18
+ *
19
+ * Rules (relay openclaw-dm-recall, 10-08):
20
+ * - only on hosts >= 2026.8.1 (older ones have neither the key nor the recall);
21
+ * - only when neither layer sets it; an explicit `true` is the owner's choice:
22
+ * kept, warned once; an explicit `false` is kept silently;
23
+ * - only once per agent: a marker in the plugin's state dir records it, so
24
+ * whatever the owner changes later is never touched again.
25
+ */
26
+ import fs from "node:fs";
27
+ import path from "node:path";
28
+
29
+ export const DM_RECALL_MARKER_FILENAME = "clawchat-dm-recall-default.json";
30
+ export const DM_RECALL_MIN_HOST: readonly [number, number, number] = [2026, 8, 1];
31
+
32
+ export type DmRecallOutcome =
33
+ | "wrote-false"
34
+ | "kept-true"
35
+ | "kept-false"
36
+ | "already-handled"
37
+ | "unsupported-host"
38
+ | "agent-not-in-roster"
39
+ | "write-failed";
40
+
41
+ type Rec = Record<string, unknown>;
42
+
43
+ type MutateConfigFile = (params: {
44
+ afterWrite: { mode: "none"; reason: string };
45
+ mutate: (draft: Rec) => void;
46
+ }) => Promise<unknown>;
47
+
48
+ type Log = {
49
+ info?: (msg: string) => void;
50
+ warn?: (msg: string) => void;
51
+ error?: (msg: string) => void;
52
+ };
53
+
54
+ function isRec(value: unknown): value is Rec {
55
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
56
+ }
57
+
58
+ function normalizeId(id: unknown): string {
59
+ return typeof id === "string" ? id.trim().toLowerCase() : "";
60
+ }
61
+
62
+ /** True for a release host >= 2026.8.1. Pre-releases and unknown strings are false. */
63
+ export function hostHasRememberAcrossConversations(version: string | undefined | null): boolean {
64
+ const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec((version ?? "").trim());
65
+ if (!m) return false;
66
+ const parts = [Number(m[1]), Number(m[2]), Number(m[3])];
67
+ for (let i = 0; i < 3; i += 1) {
68
+ if (parts[i] !== DM_RECALL_MIN_HOST[i]) return parts[i] > DM_RECALL_MIN_HOST[i];
69
+ }
70
+ return true;
71
+ }
72
+
73
+ type AgentSlot =
74
+ | { kind: "entries"; key: string; entry: Rec }
75
+ | { kind: "list"; index: number; entry: Rec }
76
+ | { kind: "no-roster" }
77
+ | { kind: "missing" };
78
+
79
+ function findAgentSlot(cfg: Rec, agentId: string): AgentSlot {
80
+ const agents = isRec(cfg.agents) ? cfg.agents : undefined;
81
+ const id = normalizeId(agentId);
82
+ if (agents && Object.hasOwn(agents, "entries") && agents.entries !== undefined) {
83
+ if (!isRec(agents.entries)) return { kind: "missing" };
84
+ for (const key of Object.keys(agents.entries)) {
85
+ const entry = agents.entries[key];
86
+ if (normalizeId(key) === id && isRec(entry)) return { kind: "entries", key, entry };
87
+ }
88
+ return { kind: "missing" };
89
+ }
90
+ if (agents && Object.hasOwn(agents, "list") && agents.list !== undefined) {
91
+ if (!Array.isArray(agents.list)) return { kind: "missing" };
92
+ const index = agents.list.findIndex((e) => isRec(e) && normalizeId(e.id) === id);
93
+ return index >= 0 ? { kind: "list", index, entry: agents.list[index] as Rec } : { kind: "missing" };
94
+ }
95
+ return { kind: "no-roster" };
96
+ }
97
+
98
+ function readFlag(holder: unknown): boolean | undefined {
99
+ if (!isRec(holder) || !isRec(holder.memory) || !isRec(holder.memory.search)) return undefined;
100
+ const v = holder.memory.search.rememberAcrossConversations;
101
+ return typeof v === "boolean" ? v : undefined;
102
+ }
103
+
104
+ /** The value written at each layer, `undefined` where it is not written. */
105
+ export function readRememberAcrossConversations(
106
+ cfg: Rec,
107
+ agentId: string,
108
+ ): { agent: boolean | undefined; global: boolean | undefined } {
109
+ const slot = findAgentSlot(cfg, agentId);
110
+ const agent = slot.kind === "entries" || slot.kind === "list" ? readFlag(slot.entry) : undefined;
111
+ return { agent, global: readFlag(cfg) };
112
+ }
113
+
114
+ function setFlagFalse(holder: Rec): void {
115
+ const memory = isRec(holder.memory) ? holder.memory : {};
116
+ const search = isRec(memory.search) ? memory.search : {};
117
+ holder.memory = { ...memory, search: { ...search, rememberAcrossConversations: false } };
118
+ }
119
+
120
+ /**
121
+ * Write `rememberAcrossConversations: false` at the agent's own layer of
122
+ * `cfg` (mutated in place). Returns the dotted path written, or `null` when a
123
+ * roster exists but does not list the agent (nothing written).
124
+ */
125
+ export function setAgentRememberAcrossConversationsFalse(cfg: Rec, agentId: string): string | null {
126
+ const slot = findAgentSlot(cfg, agentId);
127
+ if (slot.kind === "entries") {
128
+ setFlagFalse(slot.entry);
129
+ return `agents.entries.${slot.key}.memory.search`;
130
+ }
131
+ if (slot.kind === "list") {
132
+ setFlagFalse(slot.entry);
133
+ return `agents.list[${slot.index}].memory.search`;
134
+ }
135
+ if (slot.kind === "no-roster") {
136
+ setFlagFalse(cfg);
137
+ return "memory.search";
138
+ }
139
+ return null;
140
+ }
141
+
142
+ type Marker = { agents?: Record<string, { outcome: DmRecallOutcome; at: string }> };
143
+
144
+ function readMarker(file: string): Marker {
145
+ try {
146
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8")) as unknown;
147
+ return isRec(parsed) ? (parsed as Marker) : {};
148
+ } catch {
149
+ return {};
150
+ }
151
+ }
152
+
153
+ function writeMarker(file: string, agentId: string, outcome: DmRecallOutcome): void {
154
+ const marker = readMarker(file);
155
+ const agents = isRec(marker.agents) ? marker.agents : {};
156
+ agents[normalizeId(agentId)] = { outcome, at: new Date().toISOString() };
157
+ fs.mkdirSync(path.dirname(file), { recursive: true });
158
+ fs.writeFileSync(file, `${JSON.stringify({ ...marker, agents }, null, 2)}\n`);
159
+ }
160
+
161
+ /** Apply the one-time default. Never throws. */
162
+ export async function applyDmRecallDefault(params: {
163
+ agentId: string;
164
+ hostVersion: string | undefined | null;
165
+ stateDir: string;
166
+ readConfig: () => unknown;
167
+ mutateConfigFile: MutateConfigFile;
168
+ log?: Log;
169
+ }): Promise<DmRecallOutcome> {
170
+ const { agentId, log } = params;
171
+ const markerFile = path.join(params.stateDir, DM_RECALL_MARKER_FILENAME);
172
+ try {
173
+ if (readMarker(markerFile).agents?.[normalizeId(agentId)]) return "already-handled";
174
+ if (!hostHasRememberAcrossConversations(params.hostVersion)) return "unsupported-host";
175
+
176
+ const cfg = params.readConfig();
177
+ const current = readRememberAcrossConversations(isRec(cfg) ? cfg : {}, agentId);
178
+ const explicit = current.agent ?? current.global;
179
+ if (explicit === true) {
180
+ log?.warn?.(
181
+ `clawchat-plugin-openclaw: memory.search.rememberAcrossConversations is true for agent '${agentId}', so a ` +
182
+ "friend's ClawChat direct chat can recall what the owner said in theirs. Kept as set. To turn it off: " +
183
+ `openclaw config set memory.search.rememberAcrossConversations false (or set it to false on agent '${agentId}'), then restart the gateway.`,
184
+ );
185
+ writeMarker(markerFile, agentId, "kept-true");
186
+ return "kept-true";
187
+ }
188
+ if (explicit === false) {
189
+ writeMarker(markerFile, agentId, "kept-false");
190
+ return "kept-false";
191
+ }
192
+
193
+ if (findAgentSlot(isRec(cfg) ? cfg : {}, agentId).kind === "missing") {
194
+ log?.info?.(
195
+ `clawchat-plugin-openclaw: agent '${agentId}' is not in the agents roster; rememberAcrossConversations left unset`,
196
+ );
197
+ return "agent-not-in-roster";
198
+ }
199
+
200
+ let written: string | null = null;
201
+ let raced: boolean | undefined;
202
+ await params.mutateConfigFile({
203
+ afterWrite: { mode: "none", reason: "clawchat-plugin-openclaw: no recall across ClawChat direct chats" },
204
+ mutate(draft) {
205
+ // Re-check on the locked, current file: someone may have set it meanwhile.
206
+ const now = readRememberAcrossConversations(draft, agentId);
207
+ raced = now.agent ?? now.global;
208
+ if (raced !== undefined) return;
209
+ written = setAgentRememberAcrossConversationsFalse(draft, agentId);
210
+ },
211
+ });
212
+ if (written === null) {
213
+ if (raced === undefined) return "agent-not-in-roster";
214
+ const outcome: DmRecallOutcome = raced ? "kept-true" : "kept-false";
215
+ writeMarker(markerFile, agentId, outcome);
216
+ return outcome;
217
+ }
218
+ writeMarker(markerFile, agentId, "wrote-false");
219
+ log?.info?.(
220
+ `clawchat-plugin-openclaw: set ${written}.rememberAcrossConversations=false for agent '${agentId}' ` +
221
+ "(ClawChat direct chats with different people no longer recall each other)",
222
+ );
223
+ return "wrote-false";
224
+ } catch (err) {
225
+ log?.error?.(
226
+ `clawchat-plugin-openclaw: could not apply the rememberAcrossConversations default for agent '${agentId}': ${err instanceof Error ? err.message : String(err)}`,
227
+ );
228
+ return "write-failed";
229
+ }
230
+ }
package/src/runtime.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { applyDmRecallDefault } from "./dm-recall-default.ts";
1
2
  import {
2
3
  AckTimeoutError,
3
4
  AuthError,
@@ -1293,6 +1294,47 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
1293
1294
  : undefined;
1294
1295
  };
1295
1296
 
1297
+ // OpenClaw >= 2026.8.1 recalls across a single agent's DMs unless DM isolation
1298
+ // is visible in the gateway config; ours is in-memory only. Write the agent's
1299
+ // `rememberAcrossConversations: false` once, unless the owner set it
1300
+ // (dm-recall-default.ts). Fire-and-forget: never blocks or fails startup.
1301
+ void (async () => {
1302
+ const stateDir = runtime.state?.resolveStateDir?.();
1303
+ const mutateConfigFile = resolveMutateConfigFile();
1304
+ const readConfig = (runtime.config as { current?: () => unknown } | undefined)?.current;
1305
+ if (!stateDir || !mutateConfigFile || typeof readConfig !== "function") return;
1306
+ let agentId: string;
1307
+ try {
1308
+ agentId = runtime.channel.routing.resolveAgentRoute({
1309
+ cfg: withClawChatSessionScope(cfg),
1310
+ channel: CHANNEL_ID,
1311
+ accountId,
1312
+ peer: { kind: "direct", id: account.ownerUserId || "owner" },
1313
+ }).agentId;
1314
+ } catch (err) {
1315
+ log?.info?.(
1316
+ `[${accountId}] clawchat-plugin-openclaw rememberAcrossConversations default skipped: agent route unavailable (${err instanceof Error ? err.message : String(err)})`,
1317
+ );
1318
+ return;
1319
+ }
1320
+ await applyDmRecallDefault({
1321
+ agentId,
1322
+ hostVersion: runtime.version,
1323
+ stateDir,
1324
+ readConfig: () => readConfig.call(runtime.config),
1325
+ mutateConfigFile: mutateConfigFile as never,
1326
+ log: {
1327
+ info: (m) => log?.info?.(`[${accountId}] ${m}`),
1328
+ warn: (m) => {
1329
+ const warn = (log as { warn?: (msg: string) => void } | undefined)?.warn;
1330
+ if (warn) warn(`[${accountId}] ${m}`);
1331
+ else log?.info?.(`[${accountId}] WARNING ${m}`);
1332
+ },
1333
+ error: (m) => log?.error?.(`[${accountId}] ${m}`),
1334
+ },
1335
+ });
1336
+ })();
1337
+
1296
1338
  // §0/§C.1 — write the account slot's `token`/`refreshToken` keys. `tokens=null`
1297
1339
  // blanks them (auto-logout); otherwise persists the rotated pair.
1298
1340
  const persistConfigTokens = async (