@newbase-clawchat/openclaw-clawchat 2026.5.12-2 → 2026.5.12-21

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.
Files changed (99) hide show
  1. package/README.md +39 -17
  2. package/dist/index.js +3 -1
  3. package/dist/src/api-client.js +71 -12
  4. package/dist/src/api-types.test-d.js +10 -0
  5. package/dist/src/channel.js +5 -5
  6. package/dist/src/channel.setup.js +4 -17
  7. package/dist/src/clawchat-memory.js +290 -0
  8. package/dist/src/clawchat-metadata.js +235 -0
  9. package/dist/src/client.js +31 -93
  10. package/dist/src/commands.js +3 -3
  11. package/dist/src/config.js +58 -3
  12. package/dist/src/group-message-coalescer.js +107 -0
  13. package/dist/src/inbound.js +24 -28
  14. package/dist/src/login.runtime.js +82 -19
  15. package/dist/src/media-runtime.js +2 -3
  16. package/dist/src/message-mapper.js +1 -1
  17. package/dist/src/mock-transport.js +31 -0
  18. package/dist/src/outbound.js +281 -56
  19. package/dist/src/plugin-prompts.js +76 -0
  20. package/dist/src/profile-prompt.js +150 -0
  21. package/dist/src/profile-sync.js +169 -0
  22. package/dist/src/prompt-injection.js +25 -0
  23. package/dist/src/protocol-types.js +63 -0
  24. package/dist/src/protocol-types.typecheck.js +1 -0
  25. package/dist/src/protocol.js +2 -2
  26. package/dist/src/reply-dispatcher.js +143 -40
  27. package/dist/src/runtime.js +813 -109
  28. package/dist/src/storage.js +636 -0
  29. package/dist/src/tools-schema.js +70 -10
  30. package/dist/src/tools.js +600 -112
  31. package/dist/src/ws-alignment.js +8 -0
  32. package/dist/src/ws-client.js +588 -0
  33. package/index.ts +6 -1
  34. package/openclaw.plugin.json +44 -4
  35. package/package.json +4 -3
  36. package/prompts/platform.md +7 -0
  37. package/skills/clawchat/SKILL.md +90 -0
  38. package/src/api-client.test.ts +360 -15
  39. package/src/api-client.ts +127 -25
  40. package/src/api-types.test-d.ts +12 -0
  41. package/src/api-types.ts +71 -4
  42. package/src/buffered-stream.test.ts +1 -1
  43. package/src/buffered-stream.ts +1 -1
  44. package/src/channel.outbound.test.ts +270 -60
  45. package/src/channel.setup.ts +9 -18
  46. package/src/channel.test.ts +33 -25
  47. package/src/channel.ts +5 -7
  48. package/src/clawchat-memory.test.ts +372 -0
  49. package/src/clawchat-memory.ts +363 -0
  50. package/src/clawchat-metadata.test.ts +350 -0
  51. package/src/clawchat-metadata.ts +352 -0
  52. package/src/client.test.ts +57 -48
  53. package/src/client.ts +37 -129
  54. package/src/commands.test.ts +2 -2
  55. package/src/commands.ts +3 -3
  56. package/src/config.test.ts +169 -4
  57. package/src/config.ts +86 -6
  58. package/src/group-message-coalescer.test.ts +223 -0
  59. package/src/group-message-coalescer.ts +154 -0
  60. package/src/inbound.test.ts +106 -19
  61. package/src/inbound.ts +31 -35
  62. package/src/login.runtime.test.ts +294 -11
  63. package/src/login.runtime.ts +90 -21
  64. package/src/manifest.test.ts +86 -14
  65. package/src/media-runtime.test.ts +31 -2
  66. package/src/media-runtime.ts +7 -10
  67. package/src/message-mapper.test.ts +2 -2
  68. package/src/message-mapper.ts +2 -2
  69. package/src/mock-transport.test.ts +35 -0
  70. package/src/mock-transport.ts +38 -0
  71. package/src/outbound.test.ts +811 -95
  72. package/src/outbound.ts +332 -65
  73. package/src/plugin-entry.test.ts +3 -1
  74. package/src/plugin-prompts.test.ts +78 -0
  75. package/src/plugin-prompts.ts +92 -0
  76. package/src/profile-prompt.test.ts +435 -0
  77. package/src/profile-prompt.ts +208 -0
  78. package/src/profile-sync.test.ts +611 -0
  79. package/src/profile-sync.ts +268 -0
  80. package/src/prompt-injection.test.ts +39 -0
  81. package/src/prompt-injection.ts +45 -0
  82. package/src/protocol-types.test.ts +69 -0
  83. package/src/protocol-types.ts +296 -0
  84. package/src/protocol-types.typecheck.ts +89 -0
  85. package/src/protocol.ts +2 -2
  86. package/src/reply-dispatcher.test.ts +720 -135
  87. package/src/reply-dispatcher.ts +174 -42
  88. package/src/runtime.test.ts +3884 -337
  89. package/src/runtime.ts +956 -128
  90. package/src/storage.test.ts +692 -0
  91. package/src/storage.ts +989 -0
  92. package/src/streaming.test.ts +1 -1
  93. package/src/streaming.ts +1 -1
  94. package/src/tools-schema.ts +115 -13
  95. package/src/tools.test.ts +501 -10
  96. package/src/tools.ts +739 -133
  97. package/src/ws-alignment.ts +9 -0
  98. package/src/ws-client.test.ts +1218 -0
  99. package/src/ws-client.ts +662 -0
@@ -30,7 +30,7 @@ describe("runOpenclawClawlingLogin", () => {
30
30
  it("works with no prior setup (baseUrl / websocketUrl default built-in)", async () => {
31
31
  const cfg = buildCfg({}); // empty section — resolver provides defaults
32
32
  const agentsConnect = vi.fn().mockResolvedValue({
33
- agent: { user_id: "u" },
33
+ agent: { user_id: "u", owner_id: "owner-u" },
34
34
  access_token: "t",
35
35
  refresh_token: "r",
36
36
  });
@@ -106,6 +106,9 @@ describe("runOpenclawClawlingLogin", () => {
106
106
  expect(section.token).toBe("access-tok");
107
107
  expect(section.refreshToken).toBe("refresh-tok");
108
108
  expect(section.userId).toBe("agent-123");
109
+ expect(section.ownerUserId).toBe("owner-1");
110
+ expect(section.groupMode).toBe("all");
111
+ expect(section.groupCommandMode).toBe("owner");
109
112
  // Existing baseUrl and websocketUrl are preserved — agents/connect
110
113
  // does not return them.
111
114
  expect(section.baseUrl).toBe("https://api.example.com");
@@ -113,13 +116,54 @@ describe("runOpenclawClawlingLogin", () => {
113
116
  expect(log).toHaveBeenCalledWith(expect.stringContaining("login succeeded"));
114
117
  });
115
118
 
119
+ it("persists latest activation bootstrap metadata to the sqlite store after config write", async () => {
120
+ const cfg = buildCfg({
121
+ baseUrl: "https://api.example.com",
122
+ websocketUrl: "wss://ws.example.com/v2/client",
123
+ });
124
+ const agentsConnect = vi.fn().mockResolvedValue({
125
+ agent: { user_id: "agent-123", owner_id: "owner-123", nickname: "Bot" },
126
+ access_token: "access-plain",
127
+ refresh_token: "refresh-plain",
128
+ conversation: { id: "conv-activation" },
129
+ });
130
+ const persistConfig = vi.fn();
131
+ const upsertActivation = vi.fn();
132
+
133
+ await runOpenclawClawlingLogin({
134
+ cfg,
135
+ runtime: { log: vi.fn() },
136
+ readInviteCode: async () => "INV-ABC",
137
+ apiClientFactory: () => makeApiClient({ agentsConnect }),
138
+ persistConfig,
139
+ store: { upsertActivation },
140
+ });
141
+
142
+ expect(upsertActivation).toHaveBeenCalledTimes(1);
143
+ expect(persistConfig.mock.invocationCallOrder[0]).toBeLessThan(
144
+ upsertActivation.mock.invocationCallOrder[0],
145
+ );
146
+ expect(upsertActivation).toHaveBeenCalledWith({
147
+ platform: "openclaw",
148
+ accountId: "default",
149
+ userId: "agent-123",
150
+ ownerUserId: "owner-123",
151
+ conversationId: "conv-activation",
152
+ loginMethod: "login",
153
+ });
154
+ expect(upsertActivation.mock.calls[0]![0]).not.toHaveProperty("accessToken");
155
+ expect(upsertActivation.mock.calls[0]![0]).not.toHaveProperty("refreshToken");
156
+ expect(upsertActivation.mock.calls[0]![0]).not.toHaveProperty("baseUrl");
157
+ expect(upsertActivation.mock.calls[0]![0]).not.toHaveProperty("websocketUrl");
158
+ });
159
+
116
160
  it("uses the runtime config mutator with restart intent after credential writes", async () => {
117
161
  const cfg = buildCfg({
118
162
  baseUrl: "https://api.example.com",
119
163
  websocketUrl: "wss://ws.example.com/v2/client",
120
164
  });
121
165
  const agentsConnect = vi.fn().mockResolvedValue({
122
- agent: { user_id: "agent-123", nickname: "Bot" },
166
+ agent: { user_id: "agent-123", owner_id: "owner-123", nickname: "Bot" },
123
167
  access_token: "access-tok",
124
168
  refresh_token: "refresh-tok",
125
169
  });
@@ -151,6 +195,9 @@ describe("runOpenclawClawlingLogin", () => {
151
195
  expect(section.token).toBe("access-tok");
152
196
  expect(section.refreshToken).toBe("refresh-tok");
153
197
  expect(section.userId).toBe("agent-123");
198
+ expect(section.ownerUserId).toBe("owner-123");
199
+ expect(section.groupMode).toBe("all");
200
+ expect(section.groupCommandMode).toBe("owner");
154
201
  const plugins = (mutatedCfg! as OpenClawConfig & {
155
202
  plugins: { allow?: string[]; entries: Record<string, Record<string, unknown>> };
156
203
  }).plugins;
@@ -178,7 +225,7 @@ describe("runOpenclawClawlingLogin", () => {
178
225
  },
179
226
  } as unknown as OpenClawConfig;
180
227
  const agentsConnect = vi.fn().mockResolvedValue({
181
- agent: { user_id: "agent-123" },
228
+ agent: { user_id: "agent-123", owner_id: "owner-123" },
182
229
  access_token: "access-tok",
183
230
  refresh_token: "refresh-tok",
184
231
  });
@@ -202,6 +249,7 @@ describe("runOpenclawClawlingLogin", () => {
202
249
  websocketUrl: "wss://ws.example.com/v2/client",
203
250
  token: "access-tok",
204
251
  userId: "agent-123",
252
+ ownerUserId: "owner-123",
205
253
  refreshToken: "refresh-tok",
206
254
  });
207
255
  });
@@ -212,7 +260,7 @@ describe("runOpenclawClawlingLogin", () => {
212
260
  baseUrl: "https://api.example.com",
213
261
  });
214
262
  const agentsConnect = vi.fn().mockResolvedValue({
215
- agent: { user_id: "agent-123" },
263
+ agent: { user_id: "agent-123", owner_id: "owner-123" },
216
264
  access_token: "access-tok",
217
265
  refresh_token: "refresh-tok",
218
266
  });
@@ -231,6 +279,65 @@ describe("runOpenclawClawlingLogin", () => {
231
279
  expect(section.enabled).toBe(true);
232
280
  expect(section.token).toBe("access-tok");
233
281
  expect(section.userId).toBe("agent-123");
282
+ expect(section.ownerUserId).toBe("owner-123");
283
+ expect(section.groupMode).toBe("all");
284
+ });
285
+
286
+ it("does not overwrite an explicit mention groupMode during login", async () => {
287
+ const cfg = buildCfg({
288
+ baseUrl: "https://api.example.com",
289
+ groupMode: "mention",
290
+ groupCommandMode: "off",
291
+ });
292
+ const agentsConnect = vi.fn().mockResolvedValue({
293
+ agent: { user_id: "agent-123", owner_id: "owner-123" },
294
+ access_token: "access-tok",
295
+ refresh_token: "refresh-tok",
296
+ });
297
+ const persistConfig = vi.fn();
298
+
299
+ await runOpenclawClawlingLogin({
300
+ cfg,
301
+ runtime: { log: vi.fn() },
302
+ readInviteCode: async () => "INV-ABC",
303
+ apiClientFactory: () => makeApiClient({ agentsConnect }),
304
+ persistConfig,
305
+ });
306
+
307
+ const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
308
+ const section = (savedCfg.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
309
+ expect(section.groupMode).toBe("mention");
310
+ expect(section.groupCommandMode).toBe("off");
311
+ });
312
+
313
+ it("does not overwrite per-group groupMode settings during login", async () => {
314
+ const cfg = buildCfg({
315
+ baseUrl: "https://api.example.com",
316
+ groupMode: "all",
317
+ groups: {
318
+ "group-quiet": { groupMode: "mention" },
319
+ },
320
+ });
321
+ const agentsConnect = vi.fn().mockResolvedValue({
322
+ agent: { user_id: "agent-123", owner_id: "owner-123" },
323
+ access_token: "access-tok",
324
+ refresh_token: "refresh-tok",
325
+ });
326
+ const persistConfig = vi.fn();
327
+
328
+ await runOpenclawClawlingLogin({
329
+ cfg,
330
+ runtime: { log: vi.fn() },
331
+ readInviteCode: async () => "INV-ABC",
332
+ apiClientFactory: () => makeApiClient({ agentsConnect }),
333
+ persistConfig,
334
+ });
335
+
336
+ const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
337
+ const section = (savedCfg.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
338
+ expect(section.groups).toEqual({
339
+ "group-quiet": { groupMode: "mention" },
340
+ });
234
341
  });
235
342
 
236
343
  it("allows openclaw-clawchat plugin tools after successful login without replacing policy", async () => {
@@ -244,7 +351,7 @@ describe("runOpenclawClawlingLogin", () => {
244
351
  },
245
352
  } as unknown as OpenClawConfig;
246
353
  const agentsConnect = vi.fn().mockResolvedValue({
247
- agent: { user_id: "agent-123" },
354
+ agent: { user_id: "agent-123", owner_id: "owner-123" },
248
355
  access_token: "access-tok",
249
356
  refresh_token: "refresh-tok",
250
357
  });
@@ -282,7 +389,7 @@ describe("runOpenclawClawlingLogin", () => {
282
389
  },
283
390
  } as unknown as OpenClawConfig;
284
391
  const agentsConnect = vi.fn().mockResolvedValue({
285
- agent: { user_id: "agent-123" },
392
+ agent: { user_id: "agent-123", owner_id: "owner-123" },
286
393
  access_token: "access-tok",
287
394
  refresh_token: "refresh-tok",
288
395
  });
@@ -326,7 +433,7 @@ describe("runOpenclawClawlingLogin", () => {
326
433
  it("rejects responses that omit required fields", async () => {
327
434
  const cfg = buildCfg({ baseUrl: "https://api.example.com" });
328
435
  const agentsConnect = vi.fn().mockResolvedValue({
329
- agent: { user_id: "" },
436
+ agent: { user_id: "", owner_id: "owner-123" },
330
437
  access_token: "t",
331
438
  refresh_token: "r",
332
439
  });
@@ -341,13 +448,185 @@ describe("runOpenclawClawlingLogin", () => {
341
448
  ).rejects.toThrow(/missing required fields/);
342
449
  });
343
450
 
451
+ it("rejects whitespace-only required agents/connect response fields", async () => {
452
+ const cfg = buildCfg({ baseUrl: "https://api.example.com" });
453
+ const agentsConnect = vi.fn().mockResolvedValue({
454
+ agent: { user_id: " ", owner_id: "owner-123" },
455
+ access_token: "t",
456
+ refresh_token: "r",
457
+ });
458
+ await expect(
459
+ runOpenclawClawlingLogin({
460
+ cfg,
461
+ runtime: { log: vi.fn() },
462
+ readInviteCode: async () => "ok",
463
+ apiClientFactory: () => makeApiClient({ agentsConnect }),
464
+ persistConfig: vi.fn(),
465
+ }),
466
+ ).rejects.toThrow(/missing required fields/);
467
+ });
468
+
469
+ it("rejects whitespace-only agents/connect access tokens", async () => {
470
+ const cfg = buildCfg({ baseUrl: "https://api.example.com" });
471
+ const agentsConnect = vi.fn().mockResolvedValue({
472
+ agent: { user_id: "agent-123", owner_id: "owner-123" },
473
+ access_token: " ",
474
+ refresh_token: "r",
475
+ });
476
+ await expect(
477
+ runOpenclawClawlingLogin({
478
+ cfg,
479
+ runtime: { log: vi.fn() },
480
+ readInviteCode: async () => "ok",
481
+ apiClientFactory: () => makeApiClient({ agentsConnect }),
482
+ persistConfig: vi.fn(),
483
+ }),
484
+ ).rejects.toThrow(/access_token/);
485
+ });
486
+
487
+ it("rejects agents/connect responses that omit agent owner ids", async () => {
488
+ const cfg = buildCfg({ baseUrl: "https://api.example.com" });
489
+ const agentsConnect = vi.fn().mockResolvedValue({
490
+ agent: { user_id: "agent-123" },
491
+ access_token: "t",
492
+ refresh_token: "r",
493
+ });
494
+ await expect(
495
+ runOpenclawClawlingLogin({
496
+ cfg,
497
+ runtime: { log: vi.fn() },
498
+ readInviteCode: async () => "ok",
499
+ apiClientFactory: () => makeApiClient({ agentsConnect }),
500
+ persistConfig: vi.fn(),
501
+ }),
502
+ ).rejects.toThrow(/agent\.owner_id/);
503
+ });
504
+
505
+ it("rejects empty returned agent owner ids", async () => {
506
+ const cfg = buildCfg({ baseUrl: "https://api.example.com" });
507
+ const agentsConnect = vi.fn().mockResolvedValue({
508
+ agent: { user_id: "agent-123", owner_id: "" },
509
+ access_token: "t",
510
+ refresh_token: "r",
511
+ });
512
+ await expect(
513
+ runOpenclawClawlingLogin({
514
+ cfg,
515
+ runtime: { log: vi.fn() },
516
+ readInviteCode: async () => "ok",
517
+ apiClientFactory: () => makeApiClient({ agentsConnect }),
518
+ persistConfig: vi.fn(),
519
+ }),
520
+ ).rejects.toThrow(/agent\.owner_id/);
521
+ });
522
+
523
+ it("rejects whitespace-only returned agent owner ids", async () => {
524
+ const cfg = buildCfg({ baseUrl: "https://api.example.com" });
525
+ const agentsConnect = vi.fn().mockResolvedValue({
526
+ agent: { user_id: "agent-123", owner_id: " " },
527
+ access_token: "t",
528
+ refresh_token: "r",
529
+ });
530
+ await expect(
531
+ runOpenclawClawlingLogin({
532
+ cfg,
533
+ runtime: { log: vi.fn() },
534
+ readInviteCode: async () => "ok",
535
+ apiClientFactory: () => makeApiClient({ agentsConnect }),
536
+ persistConfig: vi.fn(),
537
+ }),
538
+ ).rejects.toThrow(/missing required fields/);
539
+ });
540
+
541
+ it("rejects whitespace-only activation conversation ids when present", async () => {
542
+ const cfg = buildCfg({ baseUrl: "https://api.example.com" });
543
+ const agentsConnect = vi.fn().mockResolvedValue({
544
+ agent: { user_id: "agent-123", owner_id: "owner-123" },
545
+ access_token: "t",
546
+ refresh_token: "r",
547
+ conversation: { id: " " },
548
+ });
549
+ await expect(
550
+ runOpenclawClawlingLogin({
551
+ cfg,
552
+ runtime: { log: vi.fn() },
553
+ readInviteCode: async () => "ok",
554
+ apiClientFactory: () => makeApiClient({ agentsConnect }),
555
+ persistConfig: vi.fn(),
556
+ }),
557
+ ).rejects.toThrow(/missing required fields/);
558
+ });
559
+
560
+ it("trims persisted agents/connect credentials and activation conversation id", async () => {
561
+ const cfg = buildCfg({ baseUrl: "https://api.example.com" });
562
+ const agentsConnect = vi.fn().mockResolvedValue({
563
+ agent: { user_id: " agent-123 ", owner_id: " owner-123 ", nickname: "Bot" },
564
+ access_token: " access-tok ",
565
+ refresh_token: " refresh-tok ",
566
+ conversation: { id: " conv-activation " },
567
+ });
568
+ const persistConfig = vi.fn();
569
+ const upsertActivation = vi.fn();
570
+
571
+ await runOpenclawClawlingLogin({
572
+ cfg,
573
+ runtime: { log: vi.fn() },
574
+ readInviteCode: async () => "ok",
575
+ apiClientFactory: () => makeApiClient({ agentsConnect }),
576
+ persistConfig,
577
+ store: { upsertActivation },
578
+ });
579
+
580
+ const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
581
+ const section = (savedCfg.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
582
+ expect(section.token).toBe("access-tok");
583
+ expect(section.refreshToken).toBe("refresh-tok");
584
+ expect(section.userId).toBe("agent-123");
585
+ expect(section.ownerUserId).toBe("owner-123");
586
+ expect(upsertActivation).toHaveBeenCalledWith({
587
+ platform: "openclaw",
588
+ accountId: "default",
589
+ userId: "agent-123",
590
+ ownerUserId: "owner-123",
591
+ conversationId: "conv-activation",
592
+ loginMethod: "login",
593
+ });
594
+ });
595
+
596
+ it("does not preserve a stale refresh token when agents/connect returns a blank refresh token", async () => {
597
+ const cfg = buildCfg({
598
+ baseUrl: "https://api.example.com",
599
+ refreshToken: "stale-refresh",
600
+ });
601
+ const agentsConnect = vi.fn().mockResolvedValue({
602
+ agent: { user_id: "agent-123", owner_id: "owner-123" },
603
+ access_token: "access-tok",
604
+ refresh_token: " ",
605
+ });
606
+ const persistConfig = vi.fn();
607
+
608
+ await runOpenclawClawlingLogin({
609
+ cfg,
610
+ runtime: { log: vi.fn() },
611
+ readInviteCode: async () => "ok",
612
+ apiClientFactory: () => makeApiClient({ agentsConnect }),
613
+ persistConfig,
614
+ });
615
+
616
+ const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
617
+ const section = (savedCfg.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
618
+ expect(section.token).toBe("access-tok");
619
+ expect(section.userId).toBe("agent-123");
620
+ expect(section.refreshToken).toBeUndefined();
621
+ });
622
+
344
623
  it("persists the config automatically after receiving the token (no further prompts)", async () => {
345
624
  const cfg = buildCfg({
346
625
  baseUrl: "https://api.example.com",
347
626
  websocketUrl: "wss://ws.example.com",
348
627
  });
349
628
  const agentsConnect = vi.fn().mockResolvedValue({
350
- agent: { user_id: "agent-9", nickname: "Nine" },
629
+ agent: { id: "agt-9", user_id: "agent-9", owner_id: "owner-9", nickname: "Nine" },
351
630
  access_token: "ACC-0123456789",
352
631
  refresh_token: "REF-xyz",
353
632
  });
@@ -366,7 +645,9 @@ describe("runOpenclawClawlingLogin", () => {
366
645
  expect(persistCalls).toHaveLength(1);
367
646
  const section = (persistCalls[0]!.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
368
647
  expect(section.token).toBe("ACC-0123456789");
648
+ expect(section.agentId).toBe("agt-9");
369
649
  expect(section.userId).toBe("agent-9");
650
+ expect(section.ownerUserId).toBe("owner-9");
370
651
  expect(section.refreshToken).toBe("REF-xyz");
371
652
  // Existing URL fields are preserved.
372
653
  expect(section.baseUrl).toBe("https://api.example.com");
@@ -376,12 +657,13 @@ describe("runOpenclawClawlingLogin", () => {
376
657
  expect(logMessages.some((m) => /Config file updated/.test(m))).toBe(true);
377
658
  // Token in logs is redacted.
378
659
  expect(logMessages.every((m) => !m.includes("ACC-0123456789"))).toBe(true);
660
+ expect(logMessages.every((m) => !m.includes("ACC-") && !m.includes("6789"))).toBe(true);
379
661
  });
380
662
 
381
663
  it("login logs don't leak the /agents/connect endpoint path or base URL", async () => {
382
664
  const cfg = buildCfg({ baseUrl: "https://api.example.com" });
383
665
  const agentsConnect = vi.fn().mockResolvedValue({
384
- agent: { user_id: "u" },
666
+ agent: { user_id: "u", owner_id: "owner-u" },
385
667
  access_token: "t",
386
668
  refresh_token: "r",
387
669
  });
@@ -402,7 +684,7 @@ describe("runOpenclawClawlingLogin", () => {
402
684
  it("trims the invite code before sending", async () => {
403
685
  const cfg = buildCfg({ baseUrl: "https://api.example.com" });
404
686
  const agentsConnect = vi.fn().mockResolvedValue({
405
- agent: { user_id: "u" },
687
+ agent: { user_id: "u", owner_id: "owner-u" },
406
688
  access_token: "t",
407
689
  refresh_token: "r",
408
690
  });
@@ -425,7 +707,7 @@ describe("runOpenclawClawlingLogin (non-interactive via readInviteCode)", () =>
425
707
  it("performs a full login when called with a fixed readInviteCode (programmatic path)", async () => {
426
708
  const cfg = buildCfg({ baseUrl: "https://api.example.com" });
427
709
  const agentsConnect = vi.fn().mockResolvedValue({
428
- agent: { user_id: "agent-7", nickname: "Seven" },
710
+ agent: { user_id: "agent-7", owner_id: "owner-7", nickname: "Seven" },
429
711
  access_token: "acc-non-interactive",
430
712
  refresh_token: "ref-ni",
431
713
  });
@@ -447,6 +729,7 @@ describe("runOpenclawClawlingLogin (non-interactive via readInviteCode)", () =>
447
729
  ["openclaw-clawchat"] as Record<string, unknown>;
448
730
  expect(section.token).toBe("acc-non-interactive");
449
731
  expect(section.userId).toBe("agent-7");
732
+ expect(section.ownerUserId).toBe("owner-7");
450
733
  expect(section.refreshToken).toBe("ref-ni");
451
734
  });
452
735
  });
@@ -8,6 +8,7 @@ import {
8
8
  mergeOpenclawClawchatToolAllow,
9
9
  resolveOpenclawClawlingAccount,
10
10
  } from "./config.ts";
11
+ import { getClawChatStore, type ClawChatStore } from "./storage.ts";
11
12
 
12
13
  /**
13
14
  * Platform tag sent to `/v1/agents/connect`. Identifies the host of this
@@ -43,6 +44,10 @@ export interface LoginParams {
43
44
  mutateConfigFile?: OpenclawClawchatMutateConfigFile;
44
45
  /** Test-only config persistence override. */
45
46
  persistConfig?: (cfg: OpenClawConfig) => Promise<void> | void;
47
+ /** Test/runtime override for best-effort activation persistence. */
48
+ store?: Pick<ClawChatStore, "upsertActivation">;
49
+ /** Optional database path resolved by the host runtime. */
50
+ dbPath?: string;
46
51
  }
47
52
 
48
53
  /**
@@ -73,14 +78,26 @@ async function promptInviteCodeFromStdin(runtime: {
73
78
  function buildLoginConfig(cfg: OpenClawConfig, result: AgentConnectResult): OpenClawConfig {
74
79
  const channels = (cfg.channels ?? {}) as Record<string, unknown>;
75
80
  const existing = (channels[CHANNEL_ID] ?? {}) as Record<string, unknown>;
81
+ const groupMode = existing.groupMode === "mention" || existing.groupMode === "all"
82
+ ? existing.groupMode
83
+ : "all";
84
+ const groupCommandMode = existing.groupCommandMode === "all" || existing.groupCommandMode === "off"
85
+ ? existing.groupCommandMode
86
+ : "owner";
76
87
  const nextSection: Record<string, unknown> = {
77
88
  ...existing,
78
89
  enabled: true,
90
+ groupMode,
91
+ groupCommandMode,
79
92
  token: result.access_token,
93
+ ...(result.agent.id ? { agentId: result.agent.id } : {}),
80
94
  userId: result.agent.user_id,
95
+ ownerUserId: result.agent.owner_id,
81
96
  };
82
97
  if (result.refresh_token) {
83
98
  nextSection.refreshToken = result.refresh_token;
99
+ } else {
100
+ delete nextSection.refreshToken;
84
101
  }
85
102
  return mergeOpenclawClawchatRuntimePluginActivation(
86
103
  mergeOpenclawClawchatToolAllow({
@@ -96,7 +113,7 @@ async function persistLoginConfig(
96
113
  ): Promise<void> {
97
114
  if (params.mutateConfigFile) {
98
115
  params.runtime.log(
99
- `Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id} with Gateway restart intent.`,
116
+ `Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id} with Gateway restart intent.`,
100
117
  );
101
118
  await params.mutateConfigFile({
102
119
  afterWrite: {
@@ -108,18 +125,18 @@ async function persistLoginConfig(
108
125
  },
109
126
  });
110
127
  params.runtime.log(
111
- `ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id}.`,
128
+ `ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`,
112
129
  );
113
130
  return;
114
131
  }
115
132
 
116
133
  if (params.persistConfig) {
117
134
  params.runtime.log(
118
- `Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id}.`,
135
+ `Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`,
119
136
  );
120
137
  await params.persistConfig(buildLoginConfig(params.cfg, result));
121
138
  params.runtime.log(
122
- `ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id}.`,
139
+ `ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`,
123
140
  );
124
141
  return;
125
142
  }
@@ -127,8 +144,26 @@ async function persistLoginConfig(
127
144
  throw new Error("openclaw-clawchat: mutateConfigFile is required to persist login credentials");
128
145
  }
129
146
 
147
+ function requireConnectString(value: unknown, fieldName: string): string {
148
+ if (typeof value !== "string") {
149
+ throw new Error(`agents/connect response missing required fields (${fieldName})`);
150
+ }
151
+ const trimmed = value.trim();
152
+ if (!trimmed) {
153
+ throw new Error(`agents/connect response missing required fields (${fieldName})`);
154
+ }
155
+ return trimmed;
156
+ }
157
+
158
+ function readOptionalConnectString(value: unknown, fieldName: string): string | undefined {
159
+ if (value == null) {
160
+ return undefined;
161
+ }
162
+ return requireConnectString(value, fieldName);
163
+ }
164
+
130
165
  /**
131
- * Run the invite-code credential exchange used by `/clawchat-login`,
166
+ * Run the invite-code credential exchange used by `/clawchat-activate`,
132
167
  * `openclaw channels add --channel openclaw-clawchat --token <invite-code>`,
133
168
  * and `openclaw channels login --channel openclaw-clawchat`:
134
169
  * 1. Read the existing channel section; require `baseUrl` to be set so we
@@ -178,29 +213,63 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
178
213
  throw err;
179
214
  }
180
215
 
181
- if (!result?.access_token || !result?.agent?.user_id) {
182
- throw new Error(
183
- `agents/connect response missing required fields (access_token / agent.user_id): ${JSON.stringify(result)}`,
184
- );
216
+ const accessToken = requireConnectString(result?.access_token, "access_token");
217
+ const agentUserId = requireConnectString(result?.agent?.user_id, "agent.user_id");
218
+ const ownerUserId = requireConnectString(result?.agent?.owner_id, "agent.owner_id");
219
+ const agentId = readOptionalConnectString(result?.agent?.id, "agent.id");
220
+
221
+ let conversationId: string | null = null;
222
+ if (result?.conversation != null) {
223
+ conversationId = requireConnectString(result.conversation.id, "conversation.id");
185
224
  }
186
225
 
187
- const tokenPreview = redactToken(result.access_token);
226
+ const normalizedResult: AgentConnectResult = {
227
+ ...result,
228
+ access_token: accessToken,
229
+ refresh_token: typeof result?.refresh_token === "string" ? result.refresh_token.trim() : "",
230
+ agent: {
231
+ ...result.agent,
232
+ ...(agentId ? { id: agentId } : {}),
233
+ owner_id: ownerUserId,
234
+ user_id: agentUserId,
235
+ },
236
+ ...(conversationId
237
+ ? {
238
+ conversation: {
239
+ ...result.conversation,
240
+ id: conversationId,
241
+ },
242
+ }
243
+ : {}),
244
+ };
245
+
188
246
  runtime.log(
189
- `Updating config: channels.${CHANNEL_ID}.token=${tokenPreview} userId=${result.agent.user_id}${
190
- result.refresh_token ? " refreshToken=***" : ""
247
+ `Updating config: channels.${CHANNEL_ID}.token=[REDACTED] agentId=${normalizedResult.agent.id || "-"} userId=${normalizedResult.agent.user_id} ownerUserId=${normalizedResult.agent.owner_id}${
248
+ normalizedResult.refresh_token ? " refreshToken=[REDACTED]" : ""
191
249
  } plugins.entries.${CHANNEL_ID}.enabled=true plugins.allow+=${CHANNEL_ID} …`,
192
250
  );
193
- await persistLoginConfig(params, result);
251
+ await persistLoginConfig(params, normalizedResult);
252
+ try {
253
+ const store =
254
+ params.store ??
255
+ getClawChatStore({
256
+ ...(params.dbPath ? { dbPath: params.dbPath } : {}),
257
+ log: { error: runtime.log },
258
+ });
259
+ store.upsertActivation({
260
+ platform: "openclaw",
261
+ accountId: account.accountId,
262
+ userId: normalizedResult.agent.user_id,
263
+ ownerUserId: normalizedResult.agent.owner_id,
264
+ conversationId: normalizedResult.conversation?.id ?? null,
265
+ loginMethod: "login",
266
+ });
267
+ } catch {
268
+ runtime.log("openclaw-clawchat sqlite activation persistence failed; login continues.");
269
+ }
194
270
  runtime.log(`Config file updated.`);
195
271
 
196
272
  runtime.log(
197
- `openclaw-clawchat login succeeded (user_id=${result.agent.user_id}, nickname=${result.agent.nickname || "-"}).`,
273
+ `openclaw-clawchat login succeeded (user_id=${normalizedResult.agent.user_id}, owner_user_id=${normalizedResult.agent.owner_id}, nickname=${normalizedResult.agent.nickname || "-"}).`,
198
274
  );
199
275
  }
200
-
201
- /** Shortens a token for display logs without revealing the full secret. */
202
- function redactToken(token: string): string {
203
- if (!token) return "(empty)";
204
- if (token.length <= 8) return "***";
205
- return `${token.slice(0, 4)}…${token.slice(-4)}`;
206
- }