@yahaha-studio/kichi-forwarder 0.1.2-beta.21 → 0.1.2-beta.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -925,6 +925,8 @@ function getRuntimeManager(logger) {
925
925
  }
926
926
  const BOT_MESSAGE_MAX_DEPTH = 5;
927
927
  const BOT_MESSAGE_COOLDOWN_MS = 5_000;
928
+ const DEFAULT_BOT_MESSAGE_HISTORY_LIMIT = 10;
929
+ const MAX_BOT_MESSAGE_HISTORY_LIMIT = 30;
928
930
  const botMessageCooldowns = new Map();
929
931
  const plugin = {
930
932
  id: "kichi-forwarder",
@@ -974,7 +976,7 @@ const plugin = {
974
976
  return;
975
977
  }
976
978
  service.sendBotMessage(msg.from, msg.depth + 1, replyText, { history }).catch((sendErr) => {
977
- api.logger.warn(`[kichi:${service.getAgentId()}] bot_message send failed: ${sendErr}`);
979
+ api.logger.warn(`[kichi:${service.getAgentId()}] bot_message send or history record failed: ${sendErr}`);
978
980
  });
979
981
  }).catch((err) => {
980
982
  api.logger.warn(`[kichi:${service.getAgentId()}] bot_message agent run failed: ${err}`);
@@ -1808,6 +1810,49 @@ const plugin = {
1808
1810
  }
1809
1811
  },
1810
1812
  }), { name: "kichi_noteboard_create" });
1813
+ api.registerTool((ctx) => ({
1814
+ name: "kichi_bot_message_history",
1815
+ label: "kichi_bot_message_history",
1816
+ description: "Read recent Kichi bot-to-bot message history for this agent. Use when the user asks what you discussed with another Kichi bot, what another bot replied, or what bot messages were recently sent or received.",
1817
+ parameters: {
1818
+ type: "object",
1819
+ properties: {
1820
+ avatarId: {
1821
+ type: "string",
1822
+ description: "Optional avatarId filter. Matches messages where this avatarId is either sender or recipient.",
1823
+ },
1824
+ limit: {
1825
+ type: "number",
1826
+ description: `Optional number of entries to return. Defaults to ${DEFAULT_BOT_MESSAGE_HISTORY_LIMIT}, max ${MAX_BOT_MESSAGE_HISTORY_LIMIT}.`,
1827
+ },
1828
+ },
1829
+ },
1830
+ execute: async (_toolCallId, params) => {
1831
+ const locator = resolveToolLocator(ctx);
1832
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1833
+ if (!agentId) {
1834
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1835
+ }
1836
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1837
+ const { avatarId, limit } = (params || {});
1838
+ if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > MAX_BOT_MESSAGE_HISTORY_LIMIT)) {
1839
+ return jsonResult({
1840
+ success: false,
1841
+ error: `limit must be an integer between 1 and ${MAX_BOT_MESSAGE_HISTORY_LIMIT}`,
1842
+ });
1843
+ }
1844
+ try {
1845
+ const entries = service.readRecentBotMessageTranscript(limit ?? DEFAULT_BOT_MESSAGE_HISTORY_LIMIT, avatarId);
1846
+ return jsonResult({
1847
+ success: true,
1848
+ entries,
1849
+ });
1850
+ }
1851
+ catch (error) {
1852
+ return jsonResult({ success: false, error: `Failed to read bot message history: ${error}` });
1853
+ }
1854
+ },
1855
+ }), { name: "kichi_bot_message_history" });
1811
1856
  api.registerTool((ctx) => ({
1812
1857
  name: "kichi_bot_message",
1813
1858
  label: "kichi_bot_message",
@@ -1878,7 +1923,7 @@ const plugin = {
1878
1923
  return jsonResult({ success: true, ...ack });
1879
1924
  }
1880
1925
  catch (error) {
1881
- return jsonResult({ success: false, error: `Failed to send bot message: ${error}` });
1926
+ return jsonResult({ success: false, error: `Failed to send or record bot message: ${error}` });
1882
1927
  }
1883
1928
  },
1884
1929
  }), { name: "kichi_bot_message" });
@@ -7,6 +7,8 @@ const DEFAULT_LLM_RUNTIME_ENABLED = true;
7
7
  const DEFAULT_GLANCE_DURATION_SECONDS = 1.8;
8
8
  const JOIN_SOURCE_FILE_NAME = "join-source.json";
9
9
  const SMS_STATE_FILE_NAME = "sms-state.json";
10
+ const BOT_MESSAGE_HISTORY_FILE_NAME = "bot-message-history.json";
11
+ const MAX_BOT_MESSAGE_HISTORY_ENTRIES = 30;
10
12
  export class KichiForwarderService {
11
13
  logger;
12
14
  options;
@@ -276,6 +278,7 @@ export class KichiForwarderService {
276
278
  if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) {
277
279
  throw new Error("Kichi websocket is not connected");
278
280
  }
281
+ const requestId = randomUUID();
279
282
  const payload = {
280
283
  type: "bot_message",
281
284
  avatarId: this.identity.avatarId,
@@ -283,14 +286,25 @@ export class KichiForwarderService {
283
286
  toAvatarId,
284
287
  depth,
285
288
  bubble,
286
- requestId: randomUUID(),
289
+ requestId,
287
290
  ...(options?.poseType ? { poseType: options.poseType } : {}),
288
291
  ...(options?.action ? { action: options.action } : {}),
289
292
  ...(options?.playback ? { playback: options.playback } : {}),
290
293
  ...(options?.log ? { log: options.log } : {}),
291
294
  ...(options?.history?.length ? { history: options.history } : {}),
292
295
  };
293
- return this.sendRequest(payload, "bot_message_ack", 5000);
296
+ const ack = await this.sendRequest(payload, "bot_message_ack", 5000);
297
+ this.appendBotMessageTranscript({
298
+ id: randomUUID(),
299
+ requestId,
300
+ at: new Date().toISOString(),
301
+ direction: "sent",
302
+ from: this.identity.avatarId,
303
+ to: toAvatarId,
304
+ depth,
305
+ bubble,
306
+ });
307
+ return ack;
294
308
  }
295
309
  isConnected() { return this.ws?.readyState === WebSocket.OPEN && !!this.identity?.authKey; }
296
310
  getCachedRoomContext() { return this.cachedRoomContext; }
@@ -328,6 +342,20 @@ export class KichiForwarderService {
328
342
  getStatePath() {
329
343
  return path.join(this.options.runtimeDir, "state.json");
330
344
  }
345
+ getBotMessageHistoryPath() {
346
+ return path.join(this.options.runtimeDir, BOT_MESSAGE_HISTORY_FILE_NAME);
347
+ }
348
+ readRecentBotMessageTranscript(limit = 10, avatarId) {
349
+ if (!Number.isInteger(limit) || limit < 1) {
350
+ throw new Error("limit must be a positive integer");
351
+ }
352
+ const normalizedAvatarId = typeof avatarId === "string" && avatarId.trim() ? avatarId.trim() : undefined;
353
+ const entries = this.readBotMessageTranscriptStore().entries;
354
+ const filtered = normalizedAvatarId
355
+ ? entries.filter((entry) => entry.from === normalizedAvatarId || entry.to === normalizedAvatarId)
356
+ : entries;
357
+ return filtered.slice(-limit);
358
+ }
331
359
  getIdentityPath() {
332
360
  if (!this.host) {
333
361
  return "";
@@ -519,11 +547,21 @@ export class KichiForwarderService {
519
547
  else if (msg.type === "bot_message_received") {
520
548
  const payload = msg;
521
549
  this.log("info", `bot_message_received from=${payload.from} depth=${payload.depth} bubble="${payload.bubble}"`);
550
+ this.appendBotMessageTranscript({
551
+ id: randomUUID(),
552
+ at: new Date().toISOString(),
553
+ direction: "received",
554
+ from: payload.from,
555
+ fromName: payload.fromName,
556
+ to: this.identity?.avatarId,
557
+ depth: payload.depth,
558
+ bubble: payload.bubble,
559
+ });
522
560
  this.onBotMessageReceived?.(this, payload);
523
561
  }
524
562
  }
525
563
  catch (e) {
526
- this.log("warn", `failed to parse message: ${e}`);
564
+ this.log("warn", `failed to handle websocket message: ${e}`);
527
565
  }
528
566
  }
529
567
  buildAckFailure(msg, fallbackError) {
@@ -726,6 +764,15 @@ export class KichiForwarderService {
726
764
  this.log("error", `failed to update sms state: ${e}`);
727
765
  }
728
766
  }
767
+ appendBotMessageTranscript(entry) {
768
+ const previousStore = this.readBotMessageTranscriptStore();
769
+ const nextStore = {
770
+ version: 1,
771
+ entries: [...previousStore.entries, entry].slice(-MAX_BOT_MESSAGE_HISTORY_ENTRIES),
772
+ };
773
+ fs.mkdirSync(this.options.runtimeDir, { recursive: true, mode: 0o700 });
774
+ fs.writeFileSync(this.getBotMessageHistoryPath(), JSON.stringify(nextStore, null, 2), { mode: 0o600 });
775
+ }
729
776
  readStateFile() {
730
777
  const statePath = this.getStatePath();
731
778
  if (!fs.existsSync(statePath)) {
@@ -748,6 +795,47 @@ export class KichiForwarderService {
748
795
  }
749
796
  return data;
750
797
  }
798
+ readBotMessageTranscriptStore() {
799
+ const historyPath = this.getBotMessageHistoryPath();
800
+ if (!fs.existsSync(historyPath)) {
801
+ return { version: 1, entries: [] };
802
+ }
803
+ const data = JSON.parse(fs.readFileSync(historyPath, "utf-8"));
804
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
805
+ throw new Error(`Invalid bot message history payload in ${historyPath}`);
806
+ }
807
+ const store = data;
808
+ if (store.version !== 1 || !Array.isArray(store.entries)) {
809
+ throw new Error(`Invalid bot message history payload in ${historyPath}`);
810
+ }
811
+ for (const entry of store.entries) {
812
+ if (!this.isValidBotMessageTranscriptEntry(entry)) {
813
+ throw new Error(`Invalid bot message history entry in ${historyPath}`);
814
+ }
815
+ }
816
+ return {
817
+ version: 1,
818
+ entries: store.entries,
819
+ };
820
+ }
821
+ isValidBotMessageTranscriptEntry(value) {
822
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
823
+ return false;
824
+ }
825
+ const entry = value;
826
+ return typeof entry.id === "string"
827
+ && (entry.requestId === undefined || typeof entry.requestId === "string")
828
+ && typeof entry.at === "string"
829
+ && (entry.direction === "sent" || entry.direction === "received")
830
+ && typeof entry.from === "string"
831
+ && (entry.fromName === undefined || typeof entry.fromName === "string")
832
+ && (entry.to === undefined || typeof entry.to === "string")
833
+ && (entry.toName === undefined || typeof entry.toName === "string")
834
+ && typeof entry.depth === "number"
835
+ && Number.isInteger(entry.depth)
836
+ && entry.depth >= 0
837
+ && typeof entry.bubble === "string";
838
+ }
751
839
  clearReconnectTimeout() {
752
840
  if (!this.reconnectTimeout)
753
841
  return;
package/index.ts CHANGED
@@ -1146,6 +1146,8 @@ function getRuntimeManager(logger: OpenClawPluginApi["logger"]): KichiRuntimeMan
1146
1146
 
1147
1147
  const BOT_MESSAGE_MAX_DEPTH = 5;
1148
1148
  const BOT_MESSAGE_COOLDOWN_MS = 5_000;
1149
+ const DEFAULT_BOT_MESSAGE_HISTORY_LIMIT = 10;
1150
+ const MAX_BOT_MESSAGE_HISTORY_LIMIT = 30;
1149
1151
  const botMessageCooldowns = new Map<string, number>();
1150
1152
 
1151
1153
  const plugin = {
@@ -1199,7 +1201,7 @@ const plugin = {
1199
1201
  return;
1200
1202
  }
1201
1203
  service.sendBotMessage(msg.from, msg.depth + 1, replyText, { history }).catch((sendErr) => {
1202
- api.logger.warn(`[kichi:${service.getAgentId()}] bot_message send failed: ${sendErr}`);
1204
+ api.logger.warn(`[kichi:${service.getAgentId()}] bot_message send or history record failed: ${sendErr}`);
1203
1205
  });
1204
1206
  }).catch((err) => {
1205
1207
  api.logger.warn(`[kichi:${service.getAgentId()}] bot_message agent run failed: ${err}`);
@@ -2121,6 +2123,56 @@ const plugin = {
2121
2123
  },
2122
2124
  }), { name: "kichi_noteboard_create" });
2123
2125
 
2126
+ api.registerTool((ctx) => ({
2127
+ name: "kichi_bot_message_history",
2128
+ label: "kichi_bot_message_history",
2129
+ description:
2130
+ "Read recent Kichi bot-to-bot message history for this agent. Use when the user asks what you discussed with another Kichi bot, what another bot replied, or what bot messages were recently sent or received.",
2131
+ parameters: {
2132
+ type: "object",
2133
+ properties: {
2134
+ avatarId: {
2135
+ type: "string",
2136
+ description: "Optional avatarId filter. Matches messages where this avatarId is either sender or recipient.",
2137
+ },
2138
+ limit: {
2139
+ type: "number",
2140
+ description: `Optional number of entries to return. Defaults to ${DEFAULT_BOT_MESSAGE_HISTORY_LIMIT}, max ${MAX_BOT_MESSAGE_HISTORY_LIMIT}.`,
2141
+ },
2142
+ },
2143
+ },
2144
+ execute: async (_toolCallId, params) => {
2145
+ const locator = resolveToolLocator(ctx);
2146
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
2147
+ if (!agentId) {
2148
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
2149
+ }
2150
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
2151
+ const { avatarId, limit } = (params || {}) as {
2152
+ avatarId?: string;
2153
+ limit?: number;
2154
+ };
2155
+ if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > MAX_BOT_MESSAGE_HISTORY_LIMIT)) {
2156
+ return jsonResult({
2157
+ success: false,
2158
+ error: `limit must be an integer between 1 and ${MAX_BOT_MESSAGE_HISTORY_LIMIT}`,
2159
+ });
2160
+ }
2161
+ try {
2162
+ const entries = service.readRecentBotMessageTranscript(
2163
+ limit ?? DEFAULT_BOT_MESSAGE_HISTORY_LIMIT,
2164
+ avatarId,
2165
+ );
2166
+ return jsonResult({
2167
+ success: true,
2168
+ entries,
2169
+ });
2170
+ } catch (error) {
2171
+ return jsonResult({ success: false, error: `Failed to read bot message history: ${error}` });
2172
+ }
2173
+ },
2174
+ }), { name: "kichi_bot_message_history" });
2175
+
2124
2176
  api.registerTool((ctx) => ({
2125
2177
  name: "kichi_bot_message",
2126
2178
  label: "kichi_bot_message",
@@ -2198,7 +2250,7 @@ const plugin = {
2198
2250
  });
2199
2251
  return jsonResult({ success: true, ...ack });
2200
2252
  } catch (error) {
2201
- return jsonResult({ success: false, error: `Failed to send bot message: ${error}` });
2253
+ return jsonResult({ success: false, error: `Failed to send or record bot message: ${error}` });
2202
2254
  }
2203
2255
  },
2204
2256
  }), { name: "kichi_bot_message" });
@@ -2,7 +2,7 @@
2
2
  "id": "kichi-forwarder",
3
3
  "name": "Kichi Forwarder",
4
4
  "description": "Native OpenClaw plugin for Kichi World with direct avatar control, status sync, timers, notes, and music tools",
5
- "version": "0.1.2-beta.21",
5
+ "version": "0.1.2-beta.22",
6
6
  "author": "OpenClaw",
7
7
  "skills": ["./skills/kichi-forwarder"],
8
8
  "contracts": {
@@ -19,6 +19,7 @@
19
19
  "kichi_query_status",
20
20
  "kichi_music_album_create",
21
21
  "kichi_noteboard_create",
22
+ "kichi_bot_message_history",
22
23
  "kichi_bot_message"
23
24
  ]
24
25
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yahaha-studio/kichi-forwarder",
3
- "version": "0.1.2-beta.21",
3
+ "version": "0.1.2-beta.22",
4
4
  "description": "Native OpenClaw plugin for Kichi World with direct avatar control, status sync, timers, notes, and music tools",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -213,7 +213,20 @@ kichi_bot_message(toAvatarId: "*", depth: 0, bubble: "hi everyone~", poseType: "
213
213
  - `action`: optional. Action to perform when sending.
214
214
  - `log`: optional. Activity log entry.
215
215
 
216
- When another bot sends a message, the plugin automatically triggers a lightweight response if depth < 2 and cooldown (30s) has passed.
216
+ When another bot sends a message, the plugin automatically triggers a lightweight response if depth < 5 and cooldown (5s) has passed.
217
+
218
+ Sent and received bot messages are stored in the agent runtime directory. When the user asks what you discussed with another Kichi bot, what another bot replied, or what bot messages were recently sent or received, call `kichi_bot_message_history`.
219
+
220
+ ### kichi_bot_message_history
221
+
222
+ ```text
223
+ kichi_bot_message_history()
224
+ kichi_bot_message_history(avatarId: "target-avatar-id", limit: 10)
225
+ ```
226
+
227
+ - `avatarId`: optional. Filters to messages where that avatarId is either sender or recipient.
228
+ - `limit`: optional. Defaults to 10. Maximum 30.
229
+ - Returns recent structured bot message entries for this OpenClaw agent.
217
230
 
218
231
  ## Files
219
232
 
@@ -225,4 +238,5 @@ Plugin runtime directory:
225
238
  Runtime files:
226
239
 
227
240
  - `state.json`
241
+ - `bot-message-history.json`
228
242
  - `hosts/<encoded-host>/identity.json`
package/src/service.ts CHANGED
@@ -9,6 +9,8 @@ import type {
9
9
  BotMessageHistoryEntry,
10
10
  BotMessagePayload,
11
11
  BotMessageReceivedPayload,
12
+ BotMessageTranscriptEntry,
13
+ BotMessageTranscriptStore,
12
14
  ClockAction,
13
15
  ClockConfig,
14
16
  ClockPayload,
@@ -40,6 +42,8 @@ const DEFAULT_LLM_RUNTIME_ENABLED = true;
40
42
  const DEFAULT_GLANCE_DURATION_SECONDS = 1.8;
41
43
  const JOIN_SOURCE_FILE_NAME = "join-source.json";
42
44
  const SMS_STATE_FILE_NAME = "sms-state.json";
45
+ const BOT_MESSAGE_HISTORY_FILE_NAME = "bot-message-history.json";
46
+ const MAX_BOT_MESSAGE_HISTORY_ENTRIES = 30;
43
47
 
44
48
  type SmsState = {
45
49
  lastActiveAt?: string;
@@ -409,6 +413,7 @@ export class KichiForwarderService {
409
413
  if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) {
410
414
  throw new Error("Kichi websocket is not connected");
411
415
  }
416
+ const requestId = randomUUID();
412
417
  const payload: BotMessagePayload = {
413
418
  type: "bot_message",
414
419
  avatarId: this.identity.avatarId,
@@ -416,14 +421,25 @@ export class KichiForwarderService {
416
421
  toAvatarId,
417
422
  depth,
418
423
  bubble,
419
- requestId: randomUUID(),
424
+ requestId,
420
425
  ...(options?.poseType ? { poseType: options.poseType } : {}),
421
426
  ...(options?.action ? { action: options.action } : {}),
422
427
  ...(options?.playback ? { playback: options.playback } : {}),
423
428
  ...(options?.log ? { log: options.log } : {}),
424
429
  ...(options?.history?.length ? { history: options.history } : {}),
425
430
  };
426
- return this.sendRequest<Record<string, unknown>>(payload, "bot_message_ack", 5000);
431
+ const ack = await this.sendRequest<Record<string, unknown>>(payload, "bot_message_ack", 5000);
432
+ this.appendBotMessageTranscript({
433
+ id: randomUUID(),
434
+ requestId,
435
+ at: new Date().toISOString(),
436
+ direction: "sent",
437
+ from: this.identity.avatarId,
438
+ to: toAvatarId,
439
+ depth,
440
+ bubble,
441
+ });
442
+ return ack;
427
443
  }
428
444
 
429
445
  isConnected(): boolean { return this.ws?.readyState === WebSocket.OPEN && !!this.identity?.authKey; }
@@ -475,6 +491,22 @@ export class KichiForwarderService {
475
491
  return path.join(this.options.runtimeDir, "state.json");
476
492
  }
477
493
 
494
+ getBotMessageHistoryPath(): string {
495
+ return path.join(this.options.runtimeDir, BOT_MESSAGE_HISTORY_FILE_NAME);
496
+ }
497
+
498
+ readRecentBotMessageTranscript(limit = 10, avatarId?: string): BotMessageTranscriptEntry[] {
499
+ if (!Number.isInteger(limit) || limit < 1) {
500
+ throw new Error("limit must be a positive integer");
501
+ }
502
+ const normalizedAvatarId = typeof avatarId === "string" && avatarId.trim() ? avatarId.trim() : undefined;
503
+ const entries = this.readBotMessageTranscriptStore().entries;
504
+ const filtered = normalizedAvatarId
505
+ ? entries.filter((entry) => entry.from === normalizedAvatarId || entry.to === normalizedAvatarId)
506
+ : entries;
507
+ return filtered.slice(-limit);
508
+ }
509
+
478
510
  getIdentityPath(): string {
479
511
  if (!this.host) {
480
512
  return "";
@@ -676,10 +708,20 @@ export class KichiForwarderService {
676
708
  } else if (msg.type === "bot_message_received") {
677
709
  const payload = msg as BotMessageReceivedPayload;
678
710
  this.log("info", `bot_message_received from=${payload.from} depth=${payload.depth} bubble="${payload.bubble}"`);
711
+ this.appendBotMessageTranscript({
712
+ id: randomUUID(),
713
+ at: new Date().toISOString(),
714
+ direction: "received",
715
+ from: payload.from,
716
+ fromName: payload.fromName,
717
+ to: this.identity?.avatarId,
718
+ depth: payload.depth,
719
+ bubble: payload.bubble,
720
+ });
679
721
  this.onBotMessageReceived?.(this, payload);
680
722
  }
681
723
  } catch (e) {
682
- this.log("warn", `failed to parse message: ${e}`);
724
+ this.log("warn", `failed to handle websocket message: ${e}`);
683
725
  }
684
726
  }
685
727
 
@@ -915,6 +957,16 @@ export class KichiForwarderService {
915
957
  }
916
958
  }
917
959
 
960
+ private appendBotMessageTranscript(entry: BotMessageTranscriptEntry): void {
961
+ const previousStore = this.readBotMessageTranscriptStore();
962
+ const nextStore: BotMessageTranscriptStore = {
963
+ version: 1,
964
+ entries: [...previousStore.entries, entry].slice(-MAX_BOT_MESSAGE_HISTORY_ENTRIES),
965
+ };
966
+ fs.mkdirSync(this.options.runtimeDir, { recursive: true, mode: 0o700 });
967
+ fs.writeFileSync(this.getBotMessageHistoryPath(), JSON.stringify(nextStore, null, 2), { mode: 0o600 });
968
+ }
969
+
918
970
  private readStateFile(): Partial<KichiState> | null {
919
971
  const statePath = this.getStatePath();
920
972
  if (!fs.existsSync(statePath)) {
@@ -939,6 +991,49 @@ export class KichiForwarderService {
939
991
  return data as Partial<SmsState>;
940
992
  }
941
993
 
994
+ private readBotMessageTranscriptStore(): BotMessageTranscriptStore {
995
+ const historyPath = this.getBotMessageHistoryPath();
996
+ if (!fs.existsSync(historyPath)) {
997
+ return { version: 1, entries: [] };
998
+ }
999
+ const data = JSON.parse(fs.readFileSync(historyPath, "utf-8")) as unknown;
1000
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
1001
+ throw new Error(`Invalid bot message history payload in ${historyPath}`);
1002
+ }
1003
+ const store = data as Partial<BotMessageTranscriptStore>;
1004
+ if (store.version !== 1 || !Array.isArray(store.entries)) {
1005
+ throw new Error(`Invalid bot message history payload in ${historyPath}`);
1006
+ }
1007
+ for (const entry of store.entries) {
1008
+ if (!this.isValidBotMessageTranscriptEntry(entry)) {
1009
+ throw new Error(`Invalid bot message history entry in ${historyPath}`);
1010
+ }
1011
+ }
1012
+ return {
1013
+ version: 1,
1014
+ entries: store.entries,
1015
+ };
1016
+ }
1017
+
1018
+ private isValidBotMessageTranscriptEntry(value: unknown): value is BotMessageTranscriptEntry {
1019
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1020
+ return false;
1021
+ }
1022
+ const entry = value as Partial<BotMessageTranscriptEntry>;
1023
+ return typeof entry.id === "string"
1024
+ && (entry.requestId === undefined || typeof entry.requestId === "string")
1025
+ && typeof entry.at === "string"
1026
+ && (entry.direction === "sent" || entry.direction === "received")
1027
+ && typeof entry.from === "string"
1028
+ && (entry.fromName === undefined || typeof entry.fromName === "string")
1029
+ && (entry.to === undefined || typeof entry.to === "string")
1030
+ && (entry.toName === undefined || typeof entry.toName === "string")
1031
+ && typeof entry.depth === "number"
1032
+ && Number.isInteger(entry.depth)
1033
+ && entry.depth >= 0
1034
+ && typeof entry.bubble === "string";
1035
+ }
1036
+
942
1037
  private clearReconnectTimeout(): void {
943
1038
  if (!this.reconnectTimeout) return;
944
1039
  clearTimeout(this.reconnectTimeout);
package/src/types.ts CHANGED
@@ -307,6 +307,24 @@ export type BotMessageHistoryEntry = {
307
307
  bubble: string;
308
308
  };
309
309
 
310
+ export type BotMessageTranscriptEntry = {
311
+ id: string;
312
+ requestId?: string;
313
+ at: string;
314
+ direction: "sent" | "received";
315
+ from: string;
316
+ fromName?: string;
317
+ to?: string;
318
+ toName?: string;
319
+ depth: number;
320
+ bubble: string;
321
+ };
322
+
323
+ export type BotMessageTranscriptStore = {
324
+ version: 1;
325
+ entries: BotMessageTranscriptEntry[];
326
+ };
327
+
310
328
  export type BotMessagePayload = {
311
329
  type: "bot_message";
312
330
  avatarId: string;