@clawling/clawchat-plugin-openclaw 2026.7.14-1 → 2026.7.20-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.
@@ -123,6 +123,16 @@ function pickUpdatePatch(patchInput, keys) {
123
123
  }
124
124
  return patch;
125
125
  }
126
+ function persistOwnerProfileSafe(persist, owner) {
127
+ if (!persist)
128
+ return;
129
+ try {
130
+ persist(owner);
131
+ }
132
+ catch {
133
+ // The sqlite cache is advisory — a failed write must not fail the pull.
134
+ }
135
+ }
126
136
  export async function pullOwnerMetadata(params) {
127
137
  const getAgent = params.api.getAgentDetail ?? params.api.getAgentProfile;
128
138
  if (!getAgent)
@@ -132,7 +142,9 @@ export async function pullOwnerMetadata(params) {
132
142
  const data = await getAgent(params.agentId);
133
143
  const metadata = ownerMetadataFromAgent(asRecord(data.agent) ?? {}, params);
134
144
  if (params.api.getAgentOwner) {
135
- addOwnerProfileMetadata(metadata, asRecord(await params.api.getAgentOwner()) ?? {});
145
+ const owner = asRecord(await params.api.getAgentOwner()) ?? {};
146
+ persistOwnerProfileSafe(params.persistOwnerProfile, owner);
147
+ addOwnerProfileMetadata(metadata, owner);
136
148
  }
137
149
  await writeClawChatMetadata(params.memoryRoot, { targetType: "owner", targetId: "owner" }, metadata);
138
150
  return { ok: true, writes: [{ targetType: "owner", targetId: "owner" }], failures: [] };
@@ -275,7 +287,9 @@ export async function updateMetadata(params) {
275
287
  accountUserId: params.accountUserId,
276
288
  });
277
289
  if (params.api.getAgentOwner) {
278
- addOwnerProfileMetadata(metadata, asRecord(await params.api.getAgentOwner()) ?? {});
290
+ const owner = asRecord(await params.api.getAgentOwner()) ?? {};
291
+ persistOwnerProfileSafe(params.persistOwnerProfile, owner);
292
+ addOwnerProfileMetadata(metadata, owner);
279
293
  }
280
294
  await writeClawChatMetadata(params.memoryRoot, { targetType: "owner", targetId: "owner" }, metadata);
281
295
  return { ok: true, writes: [{ targetType: "owner", targetId: "owner" }], failures: [], metadata };
@@ -284,7 +284,15 @@ export function createOpenclawClawlingReplyDispatcher(options) {
284
284
  log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw outbound media skipped: baseUrl not configured`);
285
285
  return [];
286
286
  }
287
- return await uploadOutboundMedia(urls, { apiClient, runtime, log });
287
+ const fragments = await uploadOutboundMedia(urls, { apiClient, runtime, log });
288
+ if (fragments.length < urls.length) {
289
+ // uploadOutboundMedia drops a failed item with only a log line. A shortfall
290
+ // means an attachment never reached the media service; surfacing it (parity
291
+ // with the adapter path in outbound.ts and with the Hermes plugin) stops a
292
+ // text-only frame from going out and reading as a successful send.
293
+ throw new Error(`clawchat-plugin-openclaw failed to upload ${urls.length - fragments.length}/${urls.length} outbound media attachment(s); message not sent`);
294
+ }
295
+ return fragments;
288
296
  }
289
297
  // ----- Reply state ------------------------------------------------------
290
298
  let reasoningText = "";
@@ -21,7 +21,7 @@ import { runWithTerminalClawChatSendScope } from "./terminal-send.js";
21
21
  import { flushAlignedOutboundQueue, getAlignedOutboundQueueSize, sendOpenclawClawlingText, setAlignedOutboundLogContext, } from "./outbound.js";
22
22
  import { formatWsLog } from "./ws-log.js";
23
23
  import { createNotifySignalObserver, createProtocolControlHandler, createReconnectTracker } from "./ws-alignment.js";
24
- import { clawChatDbPathForStateDir, getClawChatStore, } from "./storage.js";
24
+ import { clawChatDbPathForStateDir, getClawChatStore, makeOwnerProfilePersister, } from "./storage.js";
25
25
  import { getClawChatGroupPrompt, getClawChatUserPrompt } from "./plugin-prompts.js";
26
26
  import { loadClawChatPromptMetadata, renderClawChatProfilePrompt, resolveSenderRelation, } from "./profile-prompt.js";
27
27
  import { refreshGroupProfile, syncFirstSeenClawChatProfiles } from "./profile-sync.js";
@@ -1094,6 +1094,10 @@ export async function startOpenclawClawlingGateway(params) {
1094
1094
  log?.error?.(`[${accountId}] clawchat-plugin-openclaw metadata refresh failed source=${options.source} conversation=${conversationId}: ${err instanceof Error ? err.message : String(err)}`);
1095
1095
  }
1096
1096
  };
1097
+ const upsertOwnerProfile = store?.upsertOwnerProfile?.bind(store);
1098
+ const persistOwnerProfile = upsertOwnerProfile
1099
+ ? makeOwnerProfilePersister({ upsertOwnerProfile }, { accountId })
1100
+ : undefined;
1097
1101
  const refreshAgentBehavior = async (options) => {
1098
1102
  try {
1099
1103
  await pullOwnerMetadata({
@@ -1102,6 +1106,7 @@ export async function startOpenclawClawlingGateway(params) {
1102
1106
  accountUserId: account.userId,
1103
1107
  accountOwnerUserId: account.ownerUserId,
1104
1108
  api: getConversationApiClient(),
1109
+ persistOwnerProfile,
1105
1110
  });
1106
1111
  }
1107
1112
  catch (err) {
@@ -1173,6 +1178,7 @@ export async function startOpenclawClawlingGateway(params) {
1173
1178
  accountUserId: account.userId,
1174
1179
  accountOwnerUserId: account.ownerUserId,
1175
1180
  api: getConversationApiClient(),
1181
+ persistOwnerProfile,
1176
1182
  });
1177
1183
  return;
1178
1184
  }
@@ -1183,6 +1189,7 @@ export async function startOpenclawClawlingGateway(params) {
1183
1189
  accountUserId: account.userId,
1184
1190
  accountOwnerUserId: account.ownerUserId,
1185
1191
  api: getConversationApiClient(),
1192
+ persistOwnerProfile,
1186
1193
  });
1187
1194
  return;
1188
1195
  }
@@ -154,6 +154,23 @@ CREATE TABLE IF NOT EXISTS liveware_sample (
154
154
  updated_at INTEGER NOT NULL,
155
155
  PRIMARY KEY (platform, account_id)
156
156
  );
157
+ `,
158
+ },
159
+ {
160
+ version: 9,
161
+ name: "owner_profile",
162
+ sql: `
163
+ CREATE TABLE IF NOT EXISTS owner_profile (
164
+ platform TEXT NOT NULL,
165
+ account_id TEXT NOT NULL,
166
+ owner_user_id TEXT,
167
+ nickname TEXT,
168
+ avatar_url TEXT,
169
+ bio TEXT,
170
+ locale TEXT,
171
+ updated_at INTEGER NOT NULL,
172
+ PRIMARY KEY (platform, account_id)
173
+ );
157
174
  `,
158
175
  },
159
176
  ];
@@ -495,6 +512,30 @@ export class ClawChatStore {
495
512
  : null;
496
513
  }) ?? null;
497
514
  }
515
+ getOwnerProfile(input) {
516
+ return this.read(() => {
517
+ const row = this.requireDb()
518
+ .prepare(`SELECT * FROM owner_profile WHERE platform = ? AND account_id = ?`)
519
+ .get(input.platform, input.accountId);
520
+ return row ?? null;
521
+ }) ?? null;
522
+ }
523
+ upsertOwnerProfile(input) {
524
+ this.write(() => {
525
+ this.requireDb()
526
+ .prepare(`INSERT INTO owner_profile
527
+ (platform, account_id, owner_user_id, nickname, avatar_url, bio, locale, updated_at)
528
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
529
+ ON CONFLICT(platform, account_id) DO UPDATE SET
530
+ owner_user_id = excluded.owner_user_id,
531
+ nickname = excluded.nickname,
532
+ avatar_url = excluded.avatar_url,
533
+ bio = excluded.bio,
534
+ locale = excluded.locale,
535
+ updated_at = excluded.updated_at`)
536
+ .run(input.platform, input.accountId, input.ownerUserId ?? null, input.nickname ?? null, input.avatarUrl ?? null, input.bio ?? null, input.locale ?? null, Date.now());
537
+ });
538
+ }
498
539
  getLivewareSample(input) {
499
540
  return this.read(() => {
500
541
  const row = this.requireDb()
@@ -701,3 +742,27 @@ export function resetClawChatStoreForTest() {
701
742
  singleton?.close();
702
743
  singleton = null;
703
744
  }
745
+ /**
746
+ * Bind a persist callback for pullOwnerMetadata/updateMetadata.
747
+ *
748
+ * Maps the flat GET /v1/agents/me/owner payload onto the owner_profile
749
+ * cache row. Missing fields are stored as NULL (never empty strings).
750
+ */
751
+ export function makeOwnerProfilePersister(store, input) {
752
+ const text = (value) => {
753
+ if (value == null)
754
+ return null;
755
+ return typeof value === "string" ? value : String(value);
756
+ };
757
+ return (owner) => {
758
+ store.upsertOwnerProfile({
759
+ platform: input.platform ?? "openclaw",
760
+ accountId: input.accountId,
761
+ ownerUserId: text(owner.id),
762
+ nickname: text(owner.nickname),
763
+ avatarUrl: text(owner.avatar_url),
764
+ bio: text(owner.bio),
765
+ locale: text(owner.locale),
766
+ });
767
+ };
768
+ }
package/dist/src/tools.js CHANGED
@@ -6,7 +6,7 @@ import { resolveLivewarePath } from "./liveware-cli.js";
6
6
  import { ClawlingApiError, } from "./api-types.js";
7
7
  import { mapGateOutcome } from "./gate-outcome.js";
8
8
  import { resolveOpenclawClawlingAccount } from "./config.js";
9
- import { clawChatDbPathForStateDir, getClawChatStore, } from "./storage.js";
9
+ import { clawChatDbPathForStateDir, getClawChatStore, makeOwnerProfilePersister, } from "./storage.js";
10
10
  import { sendOpenclawClawlingMentionMessage } from "./outbound.js";
11
11
  import { getOpenclawClawlingClient, } from "./runtime.js";
12
12
  import { markTerminalClawChatSend } from "./terminal-send.js";
@@ -612,6 +612,9 @@ export function registerOpenclawClawlingTools(api, options = {}) {
612
612
  accountUserId: account.userId,
613
613
  patch: p.patch,
614
614
  api: built.client,
615
+ persistOwnerProfile: makeOwnerProfilePersister(getClawChatStore(), {
616
+ accountId: account.accountId,
617
+ }),
615
618
  }));
616
619
  }
617
620
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawling/clawchat-plugin-openclaw",
3
- "version": "2026.7.14-1",
3
+ "version": "2026.7.20-1",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "license": "MIT",
6
6
  "author": "CLAWLING PTE. LTD.",
@@ -190,12 +190,28 @@ function pickUpdatePatch<T extends Record<string, string>>(
190
190
  return patch as T;
191
191
  }
192
192
 
193
+ /** Best-effort last-known-owner-profile cache write; must never break the pull. */
194
+ export type PersistOwnerProfile = (owner: Record<string, unknown>) => void;
195
+
196
+ function persistOwnerProfileSafe(
197
+ persist: PersistOwnerProfile | undefined,
198
+ owner: Record<string, unknown>,
199
+ ): void {
200
+ if (!persist) return;
201
+ try {
202
+ persist(owner);
203
+ } catch {
204
+ // The sqlite cache is advisory — a failed write must not fail the pull.
205
+ }
206
+ }
207
+
193
208
  export async function pullOwnerMetadata(params: {
194
209
  memoryRoot: string;
195
210
  agentId: string;
196
211
  accountUserId?: string;
197
212
  accountOwnerUserId?: string;
198
213
  api: MetadataApi;
214
+ persistOwnerProfile?: PersistOwnerProfile;
199
215
  }): Promise<MetadataPullResult> {
200
216
  const getAgent = params.api.getAgentDetail ?? params.api.getAgentProfile;
201
217
  if (!getAgent) throw new Error("ClawChat metadata pull requires getAgentDetail");
@@ -203,7 +219,9 @@ export async function pullOwnerMetadata(params: {
203
219
  const data = await getAgent(params.agentId);
204
220
  const metadata = ownerMetadataFromAgent(asRecord(data.agent) ?? {}, params);
205
221
  if (params.api.getAgentOwner) {
206
- addOwnerProfileMetadata(metadata, asRecord(await params.api.getAgentOwner()) ?? {});
222
+ const owner = asRecord(await params.api.getAgentOwner()) ?? {};
223
+ persistOwnerProfileSafe(params.persistOwnerProfile, owner);
224
+ addOwnerProfileMetadata(metadata, owner);
207
225
  }
208
226
  await writeClawChatMetadata(params.memoryRoot, { targetType: "owner", targetId: "owner" }, metadata);
209
227
  return { ok: true, writes: [{ targetType: "owner", targetId: "owner" }], failures: [] };
@@ -375,6 +393,7 @@ export async function updateMetadata(params: {
375
393
  accountUserId?: string;
376
394
  patch: Record<string, unknown>;
377
395
  api: MetadataApi;
396
+ persistOwnerProfile?: PersistOwnerProfile;
378
397
  }): Promise<MetadataPushResult> {
379
398
  if (params.targetType === "owner") {
380
399
  if (!params.agentId) throw new Error("ClawChat owner metadata update requires agentId");
@@ -390,7 +409,9 @@ export async function updateMetadata(params: {
390
409
  accountUserId: params.accountUserId,
391
410
  });
392
411
  if (params.api.getAgentOwner) {
393
- addOwnerProfileMetadata(metadata, asRecord(await params.api.getAgentOwner()) ?? {});
412
+ const owner = asRecord(await params.api.getAgentOwner()) ?? {};
413
+ persistOwnerProfileSafe(params.persistOwnerProfile, owner);
414
+ addOwnerProfileMetadata(metadata, owner);
394
415
  }
395
416
  await writeClawChatMetadata(params.memoryRoot, { targetType: "owner", targetId: "owner" }, metadata);
396
417
  return { ok: true, writes: [{ targetType: "owner", targetId: "owner" }], failures: [], metadata };
@@ -418,7 +418,17 @@ export function createOpenclawClawlingReplyDispatcher(options: ReplyDispatcherOp
418
418
  );
419
419
  return [];
420
420
  }
421
- return await uploadOutboundMedia(urls, { apiClient, runtime, log });
421
+ const fragments = await uploadOutboundMedia(urls, { apiClient, runtime, log });
422
+ if (fragments.length < urls.length) {
423
+ // uploadOutboundMedia drops a failed item with only a log line. A shortfall
424
+ // means an attachment never reached the media service; surfacing it (parity
425
+ // with the adapter path in outbound.ts and with the Hermes plugin) stops a
426
+ // text-only frame from going out and reading as a successful send.
427
+ throw new Error(
428
+ `clawchat-plugin-openclaw failed to upload ${urls.length - fragments.length}/${urls.length} outbound media attachment(s); message not sent`,
429
+ );
430
+ }
431
+ return fragments;
422
432
  }
423
433
 
424
434
  // ----- Reply state ------------------------------------------------------
package/src/runtime.ts CHANGED
@@ -62,6 +62,7 @@ import { createNotifySignalObserver, createProtocolControlHandler, createReconne
62
62
  import {
63
63
  clawChatDbPathForStateDir,
64
64
  getClawChatStore,
65
+ makeOwnerProfilePersister,
65
66
  type ClawChatStore,
66
67
  type MessageRow,
67
68
  } from "./storage.ts";
@@ -118,6 +119,7 @@ type RuntimeConnectionStore = Pick<
118
119
  | "getActivationConversation"
119
120
  | "getLastResolvedDeviceId"
120
121
  | "listRecentGroupMessages"
122
+ | "upsertOwnerProfile"
121
123
  >
122
124
  >;
123
125
 
@@ -1447,6 +1449,10 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
1447
1449
  );
1448
1450
  }
1449
1451
  };
1452
+ const upsertOwnerProfile = store?.upsertOwnerProfile?.bind(store);
1453
+ const persistOwnerProfile = upsertOwnerProfile
1454
+ ? makeOwnerProfilePersister({ upsertOwnerProfile }, { accountId })
1455
+ : undefined;
1450
1456
  const refreshAgentBehavior = async (options: {
1451
1457
  metadataVersion?: number;
1452
1458
  source: string;
@@ -1459,6 +1465,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
1459
1465
  accountUserId: account.userId,
1460
1466
  accountOwnerUserId: account.ownerUserId,
1461
1467
  api: getConversationApiClient(),
1468
+ persistOwnerProfile,
1462
1469
  });
1463
1470
  } catch (err) {
1464
1471
  log?.error?.(
@@ -1533,6 +1540,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
1533
1540
  accountUserId: account.userId,
1534
1541
  accountOwnerUserId: account.ownerUserId,
1535
1542
  api: getConversationApiClient(),
1543
+ persistOwnerProfile,
1536
1544
  });
1537
1545
  return;
1538
1546
  }
@@ -1543,6 +1551,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
1543
1551
  accountUserId: account.userId,
1544
1552
  accountOwnerUserId: account.ownerUserId,
1545
1553
  api: getConversationApiClient(),
1554
+ persistOwnerProfile,
1546
1555
  });
1547
1556
  return;
1548
1557
  }
package/src/storage.ts CHANGED
@@ -191,6 +191,27 @@ export type UpdateLivewareSampleStatusInput = {
191
191
  lastError?: string | null;
192
192
  };
193
193
 
194
+ export type OwnerProfileRow = {
195
+ platform: string;
196
+ account_id: string;
197
+ owner_user_id: string | null;
198
+ nickname: string | null;
199
+ avatar_url: string | null;
200
+ bio: string | null;
201
+ locale: string | null;
202
+ updated_at: number;
203
+ };
204
+
205
+ export type OwnerProfileUpsert = {
206
+ platform: string;
207
+ accountId: string;
208
+ ownerUserId?: string | null;
209
+ nickname?: string | null;
210
+ avatarUrl?: string | null;
211
+ bio?: string | null;
212
+ locale?: string | null;
213
+ };
214
+
194
215
  type Migration = { version: number; name: string; sql: string };
195
216
 
196
217
  const DB_FILENAME = "clawchat.sqlite";
@@ -345,6 +366,23 @@ CREATE TABLE IF NOT EXISTS liveware_sample (
345
366
  updated_at INTEGER NOT NULL,
346
367
  PRIMARY KEY (platform, account_id)
347
368
  );
369
+ `,
370
+ },
371
+ {
372
+ version: 9,
373
+ name: "owner_profile",
374
+ sql: `
375
+ CREATE TABLE IF NOT EXISTS owner_profile (
376
+ platform TEXT NOT NULL,
377
+ account_id TEXT NOT NULL,
378
+ owner_user_id TEXT,
379
+ nickname TEXT,
380
+ avatar_url TEXT,
381
+ bio TEXT,
382
+ locale TEXT,
383
+ updated_at INTEGER NOT NULL,
384
+ PRIMARY KEY (platform, account_id)
385
+ );
348
386
  `,
349
387
  },
350
388
  ];
@@ -865,6 +903,43 @@ export class ClawChatStore {
865
903
  }) ?? null;
866
904
  }
867
905
 
906
+ getOwnerProfile(input: ConversationAccountInput): OwnerProfileRow | null {
907
+ return this.read(() => {
908
+ const row = this.requireDb()
909
+ .prepare(`SELECT * FROM owner_profile WHERE platform = ? AND account_id = ?`)
910
+ .get(input.platform, input.accountId);
911
+ return (row as OwnerProfileRow | undefined) ?? null;
912
+ }) ?? null;
913
+ }
914
+
915
+ upsertOwnerProfile(input: OwnerProfileUpsert): void {
916
+ this.write(() => {
917
+ this.requireDb()
918
+ .prepare(
919
+ `INSERT INTO owner_profile
920
+ (platform, account_id, owner_user_id, nickname, avatar_url, bio, locale, updated_at)
921
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
922
+ ON CONFLICT(platform, account_id) DO UPDATE SET
923
+ owner_user_id = excluded.owner_user_id,
924
+ nickname = excluded.nickname,
925
+ avatar_url = excluded.avatar_url,
926
+ bio = excluded.bio,
927
+ locale = excluded.locale,
928
+ updated_at = excluded.updated_at`,
929
+ )
930
+ .run(
931
+ input.platform,
932
+ input.accountId,
933
+ input.ownerUserId ?? null,
934
+ input.nickname ?? null,
935
+ input.avatarUrl ?? null,
936
+ input.bio ?? null,
937
+ input.locale ?? null,
938
+ Date.now(),
939
+ );
940
+ });
941
+ }
942
+
868
943
  getLivewareSample(input: ConversationAccountInput): LivewareSampleRow | null {
869
944
  return this.read(() => {
870
945
  const row = this.requireDb()
@@ -1130,3 +1205,30 @@ export function resetClawChatStoreForTest(): void {
1130
1205
  singleton?.close();
1131
1206
  singleton = null;
1132
1207
  }
1208
+
1209
+ /**
1210
+ * Bind a persist callback for pullOwnerMetadata/updateMetadata.
1211
+ *
1212
+ * Maps the flat GET /v1/agents/me/owner payload onto the owner_profile
1213
+ * cache row. Missing fields are stored as NULL (never empty strings).
1214
+ */
1215
+ export function makeOwnerProfilePersister(
1216
+ store: { upsertOwnerProfile(input: OwnerProfileUpsert): void },
1217
+ input: { accountId: string; platform?: string },
1218
+ ): (owner: Record<string, unknown>) => void {
1219
+ const text = (value: unknown): string | null => {
1220
+ if (value == null) return null;
1221
+ return typeof value === "string" ? value : String(value);
1222
+ };
1223
+ return (owner) => {
1224
+ store.upsertOwnerProfile({
1225
+ platform: input.platform ?? "openclaw",
1226
+ accountId: input.accountId,
1227
+ ownerUserId: text(owner.id),
1228
+ nickname: text(owner.nickname),
1229
+ avatarUrl: text(owner.avatar_url),
1230
+ bio: text(owner.bio),
1231
+ locale: text(owner.locale),
1232
+ });
1233
+ };
1234
+ }
package/src/tools.ts CHANGED
@@ -14,6 +14,7 @@ import { resolveOpenclawClawlingAccount } from "./config.ts";
14
14
  import {
15
15
  clawChatDbPathForStateDir,
16
16
  getClawChatStore,
17
+ makeOwnerProfilePersister,
17
18
  type ClawChatStore,
18
19
  } from "./storage.ts";
19
20
  import { sendOpenclawClawlingMentionMessage } from "./outbound.ts";
@@ -773,6 +774,9 @@ export function registerOpenclawClawlingTools(
773
774
  accountUserId: account.userId,
774
775
  patch: p.patch,
775
776
  api: built.client,
777
+ persistOwnerProfile: makeOwnerProfilePersister(getClawChatStore(), {
778
+ accountId: account.accountId,
779
+ }),
776
780
  }),
777
781
  );
778
782
  } catch (err) {