ei-tui 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -92,7 +92,11 @@ More information can be found in the [Web Readme](web/README.md)
92
92
 
93
93
  ### TUI
94
94
 
95
- ```
95
+ ```bash
96
+ # Install Bun (if you don't have it)
97
+ curl -fsSL https://bun.sh/install | bash
98
+
99
+ # Install Ei
96
100
  npm install -g ei-tui
97
101
  ```
98
102
 
@@ -126,7 +130,7 @@ This project is separated into five (5) logical parts:
126
130
 
127
131
  ## Requirements
128
132
 
129
- - [Bun](https://bun.sh) runtime (>=1.0.0)
133
+ - [Bun](https://bun.sh) runtime (>=1.0.0) — install with `curl -fsSL https://bun.sh/install | bash`
130
134
  - A local LLM (LM Studio, Ollama, etc.) OR API access to a cloud provider (Anthropic, OpenAI, Bedrock, your uncle's LLM farm, etc.)
131
135
 
132
136
  ## LM Studio Setup
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "ei-tui",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "author": "Flare576",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "git+https://github.com/flare576/ei.git"
7
+ "url": "git+https://github.com/Flare576/ei.git"
8
8
  },
9
9
  "engines": {
10
10
  "bun": ">=1.0.0"
@@ -64,4 +64,4 @@
64
64
  "solid-js": "1.9.9",
65
65
  "yaml": "^2.8.2"
66
66
  }
67
- }
67
+ }
package/src/cli/README.md CHANGED
@@ -1,4 +1,6 @@
1
1
  # The CLI
2
+
3
+ > For installation, see the [TUI README](../../tui/README.md#installation).
2
4
  ```sh
3
5
  ei # Start the TUI
4
6
  ei "query string" # Return up to 10 results across all types
@@ -23,6 +23,7 @@ import type {
23
23
  PersonaTopicMatchResult,
24
24
  PersonaTopicUpdateResult,
25
25
  } from "../../prompts/persona/types.js";
26
+ import type { PersonaResponseResult } from "../../prompts/response/index.js";
26
27
 
27
28
  import type {
28
29
  PersonaExpireResult,
@@ -106,20 +107,66 @@ function handlePersonaResponse(response: LLMResponse, state: StateManager): void
106
107
  // the messages were "seen" and processed
107
108
  state.messages_markPendingAsRead(personaId);
108
109
 
110
+ // Structured JSON path: queue-processor parsed valid JSON into `parsed`
111
+ if (response.parsed !== undefined) {
112
+ const result = response.parsed as PersonaResponseResult;
113
+
114
+ if (!result.should_respond) {
115
+ const reason = result.reason;
116
+ if (reason) {
117
+ console.log(`[handlePersonaResponse] ${personaDisplayName} chose silence: ${reason}`);
118
+ const silentMessage: Message = {
119
+ id: crypto.randomUUID(),
120
+ role: "system",
121
+ silence_reason: reason,
122
+ timestamp: new Date().toISOString(),
123
+ read: false,
124
+ context_status: ContextStatus.Default,
125
+ };
126
+ state.messages_append(personaId, silentMessage);
127
+ } else {
128
+ console.log(`[handlePersonaResponse] ${personaDisplayName} chose not to respond (no reason given)`);
129
+ }
130
+ return;
131
+ }
132
+
133
+ // Build message with structured fields
134
+ const verbal = result.verbal_response || undefined;
135
+ const action = result.action_response || undefined;
136
+
137
+ if (!verbal && !action) {
138
+ console.log(`[handlePersonaResponse] ${personaDisplayName} JSON had should_respond=true but no content fields`);
139
+ return;
140
+ }
141
+
142
+ const message: Message = {
143
+ id: crypto.randomUUID(),
144
+ role: "system",
145
+ verbal_response: verbal,
146
+ action_response: action,
147
+ timestamp: new Date().toISOString(),
148
+ read: false,
149
+ context_status: ContextStatus.Default,
150
+ };
151
+ state.messages_append(personaId, message);
152
+ console.log(`[handlePersonaResponse] Appended structured response to ${personaDisplayName}`);
153
+ return;
154
+ }
155
+
156
+ // Legacy plain-text fallback
109
157
  if (!response.content) {
110
- console.log(`[handlePersonaResponse] No content in response (${personaDisplayName} chose not to respond)`);
158
+ console.log(`[handlePersonaResponse] ${personaDisplayName} chose not to respond (no reason given)`);
111
159
  return;
112
160
  }
113
161
 
114
162
  const message: Message = {
115
163
  id: crypto.randomUUID(),
116
164
  role: "system",
117
- content: response.content,
165
+ verbal_response: response.content ?? undefined,
118
166
  timestamp: new Date().toISOString(),
119
167
  read: false,
120
168
  context_status: ContextStatus.Default,
121
169
  };
122
-
123
170
  state.messages_append(personaId, message);
124
171
  console.log(`[handlePersonaResponse] Appended response to ${personaDisplayName}`);
125
172
  }
@@ -150,7 +197,7 @@ function handleHeartbeatCheck(response: LLMResponse, state: StateManager): void
150
197
  const message: Message = {
151
198
  id: crypto.randomUUID(),
152
199
  role: "system",
153
- content: result.message,
200
+ verbal_response: result.message,
154
201
  timestamp: now,
155
202
  read: false,
156
203
  context_status: ContextStatus.Default,
@@ -179,10 +226,10 @@ function handleEiHeartbeat(response: LLMResponse, state: StateManager): void {
179
226
  return;
180
227
  }
181
228
 
182
- const sendMessage = (content: string) => state.messages_append("ei", {
229
+ const sendMessage = (verbal_response: string) => state.messages_append("ei", {
183
230
  id: crypto.randomUUID(),
184
231
  role: "system",
185
- content,
232
+ verbal_response,
186
233
  timestamp: now,
187
234
  read: false,
188
235
  context_status: ContextStatus.Default,
@@ -731,6 +778,18 @@ async function handleHumanItemUpdate(response: LLMResponse, state: StateManager)
731
778
  console.log(`[handleHumanItemUpdate] ${isNewItem ? "Created" : "Updated"} ${candidateType} "${result.name}"`);
732
779
  }
733
780
 
781
+ /**
782
+ * Returns the combined display text of a message for quote indexing.
783
+ * Mirrors the rendering logic used in the frontends.
784
+ */
785
+ function getMessageText(message: Message): string {
786
+ const parts: string[] = [];
787
+ if (message.action_response) parts.push(`_${message.action_response}_`);
788
+ if (message.verbal_response) parts.push(message.verbal_response);
789
+ return parts.join('\n\n');
790
+ }
791
+
792
+
734
793
  async function validateAndStoreQuotes(
735
794
  candidates: Array<{ text: string; reason: string }> | undefined,
736
795
  messages: Message[],
@@ -744,22 +803,53 @@ async function validateAndStoreQuotes(
744
803
  for (const candidate of candidates) {
745
804
  let found = false;
746
805
  for (const message of messages) {
747
- const start = message.content.indexOf(candidate.text);
806
+ const msgText = getMessageText(message);
807
+ const start = msgText.indexOf(candidate.text);
748
808
  if (start !== -1) {
749
809
  const end = start + candidate.text.length;
750
810
 
811
+ // Check for ANY overlapping quote in this message (not just exact match)
751
812
  const existing = state.human_quote_getForMessage(message.id);
752
- const existingQuote = existing.find(q => q.start === start && q.end === end);
813
+ const overlapping = existing.find(q =>
814
+ q.start !== null && q.end !== null &&
815
+ start < q.end && end > q.start // ranges overlap
816
+ );
753
817
 
754
- if (existingQuote) {
755
- if (!existingQuote.data_item_ids.includes(dataItemId)) {
756
- state.human_quote_update(existingQuote.id, {
757
- data_item_ids: [...existingQuote.data_item_ids, dataItemId],
758
- });
759
- console.log(`[extraction] Linked existing quote to "${dataItemId}": "${candidate.text.slice(0, 30)}..."`);
760
- } else {
761
- console.log(`[extraction] Quote already linked to "${dataItemId}": "${candidate.text.slice(0, 30)}..."`);
818
+ if (overlapping) {
819
+ // Merge: expand to the union of both ranges
820
+ const mergedStart = Math.min(start, overlapping.start!);
821
+ const mergedEnd = Math.max(end, overlapping.end!);
822
+ const mergedText = msgText.slice(mergedStart, mergedEnd);
823
+
824
+ // Merge data_item_ids and persona_groups (deduplicated)
825
+ const mergedDataItemIds = overlapping.data_item_ids.includes(dataItemId)
826
+ ? overlapping.data_item_ids
827
+ : [...overlapping.data_item_ids, dataItemId];
828
+ const group = personaGroup || "General";
829
+ const mergedGroups = overlapping.persona_groups.includes(group)
830
+ ? overlapping.persona_groups
831
+ : [...overlapping.persona_groups, group];
832
+
833
+ // Only recompute embedding if the text actually changed
834
+ let embedding = overlapping.embedding;
835
+ if (mergedText !== overlapping.text) {
836
+ try {
837
+ const embeddingService = getEmbeddingService();
838
+ embedding = await embeddingService.embed(mergedText);
839
+ } catch (err) {
840
+ console.warn(`[extraction] Failed to recompute embedding for merged quote: "${mergedText.slice(0, 30)}..."`, err);
841
+ }
762
842
  }
843
+
844
+ state.human_quote_update(overlapping.id, {
845
+ start: mergedStart,
846
+ end: mergedEnd,
847
+ text: mergedText,
848
+ data_item_ids: mergedDataItemIds,
849
+ persona_groups: mergedGroups,
850
+ embedding,
851
+ });
852
+ console.log(`[extraction] Merged overlapping quote: "${mergedText.slice(0, 50)}..." (${mergedStart}-${mergedEnd})`);
763
853
  found = true;
764
854
  break;
765
855
  }
@@ -1,4 +1,4 @@
1
- import { LLMRequestType, LLMPriority, LLMNextStep, MESSAGE_MIN_COUNT, MESSAGE_MAX_AGE_DAYS, type CeremonyConfig, type PersonaTopic, type Topic } from "../types.js";
1
+ import { LLMRequestType, LLMPriority, LLMNextStep, MESSAGE_MIN_COUNT, MESSAGE_MAX_AGE_DAYS, type CeremonyConfig, type PersonaTopic, type Topic, type Message } from "../types.js";
2
2
  import type { StateManager } from "../state-manager.js";
3
3
  import { applyDecayToValue } from "../utils/index.js";
4
4
  import {
@@ -384,12 +384,12 @@ export function queueExplorePhase(personaId: string, state: StateManager): void
384
384
  });
385
385
  }
386
386
 
387
- function extractConversationThemes(messages: { content: string; role: string }[]): string[] {
387
+ function extractConversationThemes(messages: Message[]): string[] {
388
388
  const humanMessages = messages.filter(m => m.role === "human");
389
389
  if (humanMessages.length === 0) return [];
390
390
 
391
391
  const words = humanMessages
392
- .map(m => m.content.toLowerCase())
392
+ .map(m => (m.verbal_response ?? '').toLowerCase())
393
393
  .join(" ")
394
394
  .split(/\s+/)
395
395
  .filter(w => w.length > 4);
@@ -12,7 +12,7 @@ function estimateTokens(text: string): number {
12
12
  }
13
13
 
14
14
  function estimateMessageTokens(messages: Message[]): number {
15
- return messages.reduce((sum, msg) => sum + estimateTokens(msg.content) + 4, 0);
15
+ return messages.reduce((sum, msg) => sum + estimateTokens(msg.verbal_response ?? '') + 4, 0);
16
16
  }
17
17
 
18
18
  function fitMessagesFromEnd(messages: Message[], maxTokens: number): Message[] {
@@ -20,7 +20,7 @@ function fitMessagesFromEnd(messages: Message[], maxTokens: number): Message[] {
20
20
  let tokens = 0;
21
21
 
22
22
  for (let i = messages.length - 1; i >= 0; i--) {
23
- const msgTokens = estimateTokens(messages[i].content) + 4;
23
+ const msgTokens = estimateTokens(messages[i].verbal_response ?? '') + 4;
24
24
  if (tokens + msgTokens > maxTokens) break;
25
25
  result.unshift(messages[i]);
26
26
  tokens += msgTokens;
@@ -39,7 +39,7 @@ function pullMessagesFromStart(
39
39
  let i = startIndex;
40
40
 
41
41
  while (i < messages.length) {
42
- const msgTokens = estimateTokens(messages[i].content) + 4;
42
+ const msgTokens = estimateTokens(messages[i].verbal_response ?? '') + 4;
43
43
  if (tokens + msgTokens > maxTokens && pulled.length > 0) break;
44
44
  pulled.push(messages[i]);
45
45
  tokens += msgTokens;
@@ -56,6 +56,7 @@ import {
56
56
  import { EI_WELCOME_MESSAGE, EI_PERSONA_DEFINITION } from "../templates/welcome.js";
57
57
  import { getEmbeddingService, findTopK, needsEmbeddingUpdate, needsQuoteEmbeddingUpdate, computeDataItemEmbedding, computeQuoteEmbedding } from "./embedding-service.js";
58
58
  import { ContextStatus as ContextStatusEnum } from "./types.js";
59
+ import { buildChatMessageContent } from "../prompts/message-utils.js";
59
60
 
60
61
  // =============================================================================
61
62
  // EMBEDDING STRIPPING - Remove embeddings from data items before returning to FE
@@ -124,6 +125,7 @@ export class Processor {
124
125
  private currentRequest: LLMRequest | null = null;
125
126
  private isTUI = false;
126
127
  private lastOpenCodeSync = 0;
128
+ private lastDLQTrim = 0;
127
129
  private openCodeImportInProgress = false;
128
130
  private pendingConflict: StateConflictData | null = null;
129
131
 
@@ -232,7 +234,7 @@ export class Processor {
232
234
  const welcomeMessage: Message = {
233
235
  id: crypto.randomUUID(),
234
236
  role: "system",
235
- content: EI_WELCOME_MESSAGE,
237
+ verbal_response: EI_WELCOME_MESSAGE,
236
238
  timestamp: new Date().toISOString(),
237
239
  read: false,
238
240
  context_status: ContextStatusEnum.Always,
@@ -334,31 +336,36 @@ export class Processor {
334
336
  await this.checkScheduledTasks();
335
337
 
336
338
  if (this.queueProcessor.getState() === "idle") {
337
- const request = this.stateManager.queue_peekHighest();
338
- if (request) {
339
- const personaId = request.data.personaId as string | undefined;
340
- const personaDisplayName = request.data.personaDisplayName as string | undefined;
341
- const personaSuffix = personaDisplayName ? ` [${personaDisplayName}]` : "";
342
- console.log(`[Processor ${this.instanceId}] processing request: ${request.next_step}${personaSuffix}`);
343
- this.currentRequest = request;
344
-
345
- if (personaId && request.next_step === LLMNextStep.HandlePersonaResponse) {
346
- this.interface.onMessageProcessing?.(personaId);
339
+ const retryAfter = this.stateManager.queue_nextItemRetryAfter();
340
+ const isBackingOff = retryAfter !== null && retryAfter > new Date().toISOString();
341
+
342
+ if (!isBackingOff) {
343
+ const request = this.stateManager.queue_claimHighest();
344
+ if (request) {
345
+ const personaId = request.data.personaId as string | undefined;
346
+ const personaDisplayName = request.data.personaDisplayName as string | undefined;
347
+ const personaSuffix = personaDisplayName ? ` [${personaDisplayName}]` : "";
348
+ console.log(`[Processor ${this.instanceId}] processing request: ${request.next_step}${personaSuffix}`);
349
+ this.currentRequest = request;
350
+
351
+ if (personaId && request.next_step === LLMNextStep.HandlePersonaResponse) {
352
+ this.interface.onMessageProcessing?.(personaId);
353
+ }
354
+
355
+ this.queueProcessor.start(request, async (response) => {
356
+ this.currentRequest = null;
357
+ await this.handleResponse(response);
358
+ const nextState = this.stateManager.queue_isPaused() ? "paused" : "idle";
359
+ // the processor state is set in the caller, so this needs a bit of delay
360
+ setTimeout(() => this.interface.onQueueStateChanged?.(nextState), 0);
361
+ }, {
362
+ accounts: this.stateManager.getHuman().settings?.accounts,
363
+ messageFetcher: (pName) => this.fetchMessagesForLLM(pName),
364
+ rawMessageFetcher: (pName) => this.stateManager.messages_get(pName),
365
+ });
366
+
367
+ this.interface.onQueueStateChanged?.("busy");
347
368
  }
348
-
349
- this.queueProcessor.start(request, async (response) => {
350
- this.currentRequest = null;
351
- await this.handleResponse(response);
352
- const nextState = this.stateManager.queue_isPaused() ? "paused" : "idle";
353
- // the processor state is set in the caller, so this needs a bit of delay
354
- setTimeout(() => this.interface.onQueueStateChanged?.(nextState), 0);
355
- }, {
356
- accounts: this.stateManager.getHuman().settings?.accounts,
357
- messageFetcher: (pName) => this.fetchMessagesForLLM(pName),
358
- rawMessageFetcher: (pName) => this.stateManager.messages_get(pName),
359
- });
360
-
361
- this.interface.onQueueStateChanged?.("busy");
362
369
  }
363
370
  }
364
371
 
@@ -407,6 +414,15 @@ export class Processor {
407
414
  }
408
415
  }
409
416
  }
417
+ // DLQ rolloff — once per day
418
+ const MS_PER_DAY = 86_400_000;
419
+ if (now - this.lastDLQTrim >= MS_PER_DAY) {
420
+ this.lastDLQTrim = now;
421
+ const trimmed = this.stateManager.queue_trimDLQ();
422
+ if (trimmed > 0) {
423
+ console.log(`[Processor] DLQ trim: removed ${trimmed} expired items`);
424
+ }
425
+ }
410
426
  }
411
427
 
412
428
  private async checkAndSyncOpenCode(human: HumanEntity, now: number): Promise<void> {
@@ -484,10 +500,17 @@ export class Processor {
484
500
  contextWindowHours
485
501
  );
486
502
 
487
- return filteredHistory.map((m) => ({
488
- role: m.role === "human" ? "user" : "assistant",
489
- content: m.content,
490
- })) as import("./types.js").ChatMessage[];
503
+ return filteredHistory
504
+ .reduce<import("./types.js").ChatMessage[]>((acc, m) => {
505
+ const content = buildChatMessageContent(m);
506
+ if (content.length > 0) {
507
+ acc.push({
508
+ role: m.role === "human" ? "user" : "assistant",
509
+ content,
510
+ });
511
+ }
512
+ return acc;
513
+ }, []);
491
514
  }
492
515
 
493
516
  private async queueHeartbeatCheck(personaId: string): Promise<void> {
@@ -542,7 +565,7 @@ export class Processor {
542
565
  const items: EiHeartbeatItem[] = [];
543
566
 
544
567
  const unverifiedFacts = human.facts
545
- .filter(f => f.validated === ValidationLevel.None && f.learned_by !== "ei")
568
+ .filter(f => f.validated === ValidationLevel.None && f.learned_by !== "Ei")
546
569
  .slice(0, 5);
547
570
  for (const fact of unverifiedFacts) {
548
571
  const quote = human.quotes.find(q => q.data_item_ids.includes(fact.id));
@@ -932,7 +955,7 @@ export class Processor {
932
955
  .map(m => m.id);
933
956
  if (pendingIds.length === 0) return "";
934
957
  const removed = this.stateManager.messages_remove(personaId, pendingIds);
935
- const recalledContent = removed.map(m => m.content).join("\n\n");
958
+ const recalledContent = removed.map(m => m.verbal_response ?? '').join("\n\n");
936
959
  this.interface.onMessageAdded?.(personaId);
937
960
  this.interface.onMessageRecalled?.(personaId, recalledContent);
938
961
  return recalledContent;
@@ -953,7 +976,7 @@ export class Processor {
953
976
  const message: Message = {
954
977
  id: crypto.randomUUID(),
955
978
  role: "human",
956
- content,
979
+ verbal_response: content,
957
980
  timestamp: new Date().toISOString(),
958
981
  read: false,
959
982
  context_status: "default" as ContextStatus,
@@ -1469,13 +1492,31 @@ export class Processor {
1469
1492
  return {
1470
1493
  state: this.stateManager.queue_isPaused()
1471
1494
  ? "paused"
1472
- : this.queueProcessor.getState() === "busy"
1495
+ : this.stateManager.queue_hasProcessingItem()
1473
1496
  ? "busy"
1474
1497
  : "idle",
1475
1498
  pending_count: this.stateManager.queue_length(),
1499
+ dlq_count: this.stateManager.queue_dlqLength(),
1476
1500
  };
1477
1501
  }
1478
1502
 
1503
+ pauseQueue(): void {
1504
+ this.stateManager.queue_pause();
1505
+ this.queueProcessor.abort();
1506
+ }
1507
+
1508
+ getQueueActiveItems(): LLMRequest[] {
1509
+ return this.stateManager.queue_getAllActiveItems();
1510
+ }
1511
+
1512
+ getDLQItems(): LLMRequest[] {
1513
+ return this.stateManager.queue_getDLQItems();
1514
+ }
1515
+
1516
+ updateQueueItem(id: string, updates: Partial<LLMRequest>): boolean {
1517
+ return this.stateManager.queue_updateItem(id, updates);
1518
+ }
1519
+
1479
1520
  async clearQueue(): Promise<number> {
1480
1521
  this.queueProcessor.abort();
1481
1522
  return this.stateManager.queue_clear();
@@ -1,5 +1,5 @@
1
- import type { LLMRequest, LLMResponse, LLMRequestType, ProviderAccount, ChatMessage, Message } from "./types.js";
2
- import { callLLMRaw, parseJSONResponse, cleanResponseContent } from "./llm-client.js";
1
+ import { LLMRequest, LLMResponse, LLMRequestType, ProviderAccount, ChatMessage, Message } from "./types.js";
2
+ import { callLLMRaw, parseJSONResponse } from "./llm-client.js";
3
3
  import { hydratePromptPlaceholders } from "../prompts/message-utils.js";
4
4
 
5
5
  type QueueProcessorState = "idle" | "busy";
@@ -133,9 +133,8 @@ export class QueueProcessor {
133
133
  ): LLMResponse {
134
134
  switch (request.type) {
135
135
  case "json" as LLMRequestType:
136
- return this.handleJSONResponse(request, content, finishReason);
137
136
  case "response" as LLMRequestType:
138
- return this.handleConversationResponse(request, content, finishReason);
137
+ return this.handleJSONResponse(request, content, finishReason);
139
138
  case "raw" as LLMRequestType:
140
139
  default:
141
140
  return {
@@ -172,26 +171,4 @@ export class QueueProcessor {
172
171
  }
173
172
  }
174
173
 
175
- private handleConversationResponse(
176
- request: LLMRequest,
177
- content: string,
178
- finishReason: string | null
179
- ): LLMResponse {
180
- const cleaned = cleanResponseContent(content);
181
-
182
- const noMessagePatterns = [
183
- /^no\s*(new\s*)?(message|response)/i,
184
- /^nothing\s+to\s+(say|add)/i,
185
- /^\[no\s+message\]/i,
186
- ];
187
-
188
- const isNoMessage = noMessagePatterns.some((p) => p.test(cleaned));
189
-
190
- return {
191
- request,
192
- success: true,
193
- content: isNoMessage ? null : cleaned,
194
- finish_reason: finishReason ?? undefined,
195
- };
196
174
  }
197
- }
@@ -1,5 +1,17 @@
1
1
  import type { PersonaEntity, Message, ContextStatus } from "../types.js";
2
2
 
3
+ /**
4
+ * Migration: If a persisted message has the old `content` field but no `verbal_response`,
5
+ * move content → verbal_response. Runs on every load (no-op for already-migrated data).
6
+ */
7
+ function migrateMessage(msg: Message & { content?: string }): Message {
8
+ if (msg.content !== undefined && msg.verbal_response === undefined) {
9
+ const { content, ...rest } = msg;
10
+ return { ...rest, verbal_response: content };
11
+ }
12
+ return msg;
13
+ }
14
+
3
15
  export interface PersonaData {
4
16
  entity: PersonaEntity;
5
17
  messages: Message[];
@@ -13,7 +25,7 @@ export class PersonaState {
13
25
  this.personas = new Map(
14
26
  Object.entries(personas).map(([id, data]) => [
15
27
  id,
16
- { entity: data.entity, messages: data.messages },
28
+ { entity: data.entity, messages: data.messages.map(migrateMessage) },
17
29
  ])
18
30
  );
19
31
  }