ei-tui 0.1.3 → 0.1.5

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 (44) hide show
  1. package/README.md +36 -35
  2. package/package.json +6 -2
  3. package/src/README.md +85 -1
  4. package/src/cli/README.md +30 -20
  5. package/src/cli/retrieval.ts +5 -17
  6. package/src/cli.ts +69 -0
  7. package/src/core/handlers/index.ts +195 -172
  8. package/src/core/orchestrators/ceremony.ts +4 -4
  9. package/src/core/orchestrators/extraction-chunker.ts +3 -3
  10. package/src/core/processor.ts +245 -77
  11. package/src/core/queue-processor.ts +3 -26
  12. package/src/core/state/checkpoints.ts +4 -0
  13. package/src/core/state/personas.ts +13 -1
  14. package/src/core/state/queue.ts +80 -23
  15. package/src/core/state-manager.ts +36 -10
  16. package/src/core/types.ts +23 -11
  17. package/src/core/utils/crossFind.ts +44 -0
  18. package/src/core/utils/index.ts +4 -0
  19. package/src/integrations/opencode/importer.ts +118 -691
  20. package/src/prompts/heartbeat/check.ts +27 -13
  21. package/src/prompts/heartbeat/ei.ts +65 -136
  22. package/src/prompts/heartbeat/types.ts +47 -17
  23. package/src/prompts/human/item-update.ts +20 -8
  24. package/src/prompts/index.ts +2 -5
  25. package/src/prompts/message-utils.ts +42 -3
  26. package/src/prompts/response/index.ts +13 -6
  27. package/src/prompts/response/sections.ts +65 -12
  28. package/src/prompts/response/types.ts +10 -0
  29. package/tui/README.md +89 -4
  30. package/tui/src/commands/dlq.ts +75 -0
  31. package/tui/src/commands/editor.tsx +1 -1
  32. package/tui/src/commands/queue.ts +77 -0
  33. package/tui/src/components/CommandSuggest.tsx +50 -0
  34. package/tui/src/components/MessageList.tsx +12 -2
  35. package/tui/src/components/PromptInput.tsx +118 -30
  36. package/tui/src/components/Sidebar.tsx +6 -2
  37. package/tui/src/components/StatusBar.tsx +12 -5
  38. package/tui/src/context/ei.tsx +43 -3
  39. package/tui/src/context/keyboard.tsx +90 -2
  40. package/tui/src/util/clipboard.ts +73 -0
  41. package/tui/src/util/yaml-serializers.ts +81 -11
  42. package/src/prompts/validation/ei.ts +0 -93
  43. package/src/prompts/validation/index.ts +0 -6
  44. package/src/prompts/validation/types.ts +0 -22
@@ -1,16 +1,17 @@
1
- import {
2
- ContextStatus,
3
- LLMNextStep,
1
+ import {
2
+ ContextStatus,
3
+ LLMNextStep,
4
4
  ValidationLevel,
5
- type LLMResponse,
6
- type Message,
7
- type Trait,
5
+ type LLMResponse,
6
+ type Message,
7
+ type Trait,
8
8
  type Topic,
9
9
  type PersonaTopic,
10
10
  type Fact,
11
11
  type Person,
12
12
  type Quote,
13
13
  type DataItemType,
14
+ type DataItemBase,
14
15
  } from "../types.js";
15
16
  import type { StateManager } from "../state-manager.js";
16
17
  import type { HeartbeatCheckResult, EiHeartbeatResult } from "../../prompts/heartbeat/types.js";
@@ -22,7 +23,8 @@ import type {
22
23
  PersonaTopicMatchResult,
23
24
  PersonaTopicUpdateResult,
24
25
  } from "../../prompts/persona/types.js";
25
- import type { EiValidationResult } from "../../prompts/validation/types.js";
26
+ import type { PersonaResponseResult } from "../../prompts/response/index.js";
27
+
26
28
  import type {
27
29
  PersonaExpireResult,
28
30
  PersonaExploreResult,
@@ -50,9 +52,10 @@ import type {
50
52
  ItemUpdateResult,
51
53
  ExposureImpact,
52
54
  } from "../../prompts/human/types.js";
53
- import { buildEiValidationPrompt } from "../../prompts/validation/index.js";
54
- import { LLMRequestType, LLMPriority, LLMNextStep as NextStep } from "../types.js";
55
+
56
+ import { LLMRequestType, LLMPriority } from "../types.js";
55
57
  import { getEmbeddingService, getItemEmbeddingText } from "../embedding-service.js";
58
+ import { crossFind } from "../utils/index.js";
56
59
 
57
60
  export type ResponseHandler = (response: LLMResponse, state: StateManager) => void | Promise<void>;
58
61
 
@@ -104,20 +107,66 @@ function handlePersonaResponse(response: LLMResponse, state: StateManager): void
104
107
  // the messages were "seen" and processed
105
108
  state.messages_markPendingAsRead(personaId);
106
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.Never,
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
107
157
  if (!response.content) {
108
- console.log(`[handlePersonaResponse] No content in response (${personaDisplayName} chose not to respond)`);
158
+ console.log(`[handlePersonaResponse] ${personaDisplayName} chose not to respond (no reason given)`);
109
159
  return;
110
160
  }
111
161
 
112
162
  const message: Message = {
113
163
  id: crypto.randomUUID(),
114
164
  role: "system",
115
- content: response.content,
165
+ verbal_response: response.content ?? undefined,
116
166
  timestamp: new Date().toISOString(),
117
167
  read: false,
118
168
  context_status: ContextStatus.Default,
119
169
  };
120
-
121
170
  state.messages_append(personaId, message);
122
171
  console.log(`[handlePersonaResponse] Appended response to ${personaDisplayName}`);
123
172
  }
@@ -148,7 +197,7 @@ function handleHeartbeatCheck(response: LLMResponse, state: StateManager): void
148
197
  const message: Message = {
149
198
  id: crypto.randomUUID(),
150
199
  role: "system",
151
- content: result.message,
200
+ verbal_response: result.message,
152
201
  timestamp: now,
153
202
  read: false,
154
203
  context_status: ContextStatus.Default,
@@ -164,29 +213,52 @@ function handleEiHeartbeat(response: LLMResponse, state: StateManager): void {
164
213
  console.error("[handleEiHeartbeat] No parsed result");
165
214
  return;
166
215
  }
167
-
168
216
  const now = new Date().toISOString();
169
217
  state.persona_update("ei", { last_heartbeat: now });
170
-
171
- if (!result.should_respond) {
218
+ if (!result.should_respond || !result.id) {
172
219
  console.log("[handleEiHeartbeat] Ei chose not to reach out");
173
220
  return;
174
221
  }
222
+ const isTUI = response.request.data.isTUI as boolean;
223
+ const found = crossFind(result.id, state.getHuman(), state.persona_getAll());
224
+ if (!found) {
225
+ console.warn(`[handleEiHeartbeat] Could not find item with id "${result.id}"`);
226
+ return;
227
+ }
175
228
 
176
- if (result.message) {
177
- const message: Message = {
178
- id: crypto.randomUUID(),
179
- role: "system",
180
- content: result.message,
181
- timestamp: now,
182
- read: false,
183
- context_status: ContextStatus.Default,
184
- };
185
- state.messages_append("ei", message);
186
- console.log("[handleEiHeartbeat] Ei proactively messaged");
187
- if (result.priorities) {
188
- console.log("[handleEiHeartbeat] Priorities:", result.priorities.map(p => p.name).join(", "));
189
- }
229
+ const sendMessage = (verbal_response: string) => state.messages_append("ei", {
230
+ id: crypto.randomUUID(),
231
+ role: "system",
232
+ verbal_response,
233
+ timestamp: now,
234
+ read: false,
235
+ context_status: ContextStatus.Default,
236
+ });
237
+
238
+ if (found.type === "fact") {
239
+ const factsNav = isTUI ? "using /me facts" : "using \u2630 \u2192 My Data";
240
+ sendMessage(`Another persona updated a fact called "${found.name}" to "${found.description}". If that's right, you can lock it from further changes by ${factsNav}.`);
241
+ state.human_fact_upsert({ ...found, validated: ValidationLevel.Ei, validated_date: now });
242
+ console.log(`[handleEiHeartbeat] Notified about fact "${found.name}"`);
243
+ return;
244
+ }
245
+
246
+ if (result.my_response) sendMessage(result.my_response);
247
+
248
+ switch (found.type) {
249
+ case "person":
250
+ state.human_person_upsert({ ...found, last_ei_asked: now });
251
+ console.log(`[handleEiHeartbeat] Reached out about person "${found.name}"`);
252
+ break;
253
+ case "topic":
254
+ state.human_topic_upsert({ ...found, last_ei_asked: now });
255
+ console.log(`[handleEiHeartbeat] Reached out about topic "${found.name}"`);
256
+ break;
257
+ case "persona":
258
+ console.log(`[handleEiHeartbeat] Reached out about persona "${found.display_name}"`);
259
+ break;
260
+ default:
261
+ console.warn(`[handleEiHeartbeat] Unexpected item type "${found.type}" for id "${result.id}"`);
190
262
  }
191
263
  }
192
264
 
@@ -291,59 +363,7 @@ function handlePersonaTraitExtraction(response: LLMResponse, state: StateManager
291
363
  console.log(`[handlePersonaTraitExtraction] Updated ${traits.length} traits for ${personaDisplayName}`);
292
364
  }
293
365
 
294
- function handleEiValidation(response: LLMResponse, state: StateManager): void {
295
- const validationId = response.request.data.validationId as string;
296
- const dataType = response.request.data.dataType as string;
297
- const itemName = response.request.data.itemName as string;
298
-
299
- const result = response.parsed as EiValidationResult | undefined;
300
- if (!result) {
301
- console.error("[handleEiValidation] No parsed result");
302
- return;
303
- }
304
-
305
- console.log(`[handleEiValidation] Decision for ${dataType} "${itemName}": ${result.decision} - ${result.reason}`);
306
-
307
- if (result.decision === "reject") {
308
- if (validationId) {
309
- state.queue_clearValidations([validationId]);
310
- }
311
- return;
312
- }
313
366
 
314
- const itemToApply = result.decision === "modify" && result.modified_item
315
- ? result.modified_item
316
- : response.request.data.proposedItem;
317
-
318
- if (itemToApply && dataType) {
319
- const now = new Date().toISOString();
320
- const item = { ...itemToApply, last_updated: now };
321
-
322
- switch (dataType) {
323
- case "fact":
324
- state.human_fact_upsert({
325
- ...item,
326
- validated: ValidationLevel.Ei,
327
- validated_date: now,
328
- } as Fact);
329
- break;
330
- case "trait":
331
- state.human_trait_upsert(item as Trait);
332
- break;
333
- case "topic":
334
- state.human_topic_upsert(item as Topic);
335
- break;
336
- case "person":
337
- state.human_person_upsert(item as Person);
338
- break;
339
- }
340
- console.log(`[handleEiValidation] Applied ${result.decision} for ${dataType} "${itemName}"`);
341
- }
342
-
343
- if (validationId) {
344
- state.queue_clearValidations([validationId]);
345
- }
346
- }
347
367
 
348
368
  function handleOneShot(_response: LLMResponse, _state: StateManager): void {
349
369
  // One-shot is handled specially in Processor to fire onOneShotReturned
@@ -573,43 +593,48 @@ function handleHumanItemMatch(response: LLMResponse, state: StateManager): void
573
593
  console.error("[handleHumanItemMatch] No parsed result");
574
594
  return;
575
595
  }
576
- // "new" isn't a valid guid and is used as a marker for LLMs to signify "no match" - update for processing
577
- if (result?.matched_guid === "new") {
578
- result.matched_guid = null;
579
- }
580
596
 
581
597
  const candidateType = response.request.data.candidateType as DataItemType;
582
- const itemName = response.request.data.itemName as string;
583
- const itemValue = response.request.data.itemValue as string;
584
598
  const personaId = response.request.data.personaId as string;
585
599
  const personaDisplayName = response.request.data.personaDisplayName as string;
586
600
  const analyzeFrom = response.request.data.analyze_from_timestamp as string | null;
587
-
588
601
  const allMessages = state.messages_get(personaId);
589
602
  const { messages_context, messages_analyze } = splitMessagesByTimestamp(allMessages, analyzeFrom);
590
-
591
- if (result.matched_guid) {
592
- const human = state.getHuman();
593
- const matchedFact = human.facts.find(f => f.id === result.matched_guid);
594
- if (matchedFact?.validated === ValidationLevel.Human) {
595
- console.log(`[handleHumanItemMatch] Skipping locked fact "${matchedFact.name}" (human-validated)`);
596
- return;
597
- }
598
- }
599
-
600
603
  const context: ExtractionContext & { itemName: string; itemValue: string; itemCategory?: string } = {
601
604
  personaId,
602
605
  personaDisplayName,
603
606
  messages_context,
604
607
  messages_analyze,
605
- itemName,
606
- itemValue,
607
- itemCategory: candidateType === "topic" ? itemValue : undefined,
608
+ itemName: response.request.data.itemName as string,
609
+ itemValue: response.request.data.itemValue as string,
610
+ itemCategory: response.request.data.itemCategory as string | undefined,
608
611
  };
609
612
 
610
- queueItemUpdate(candidateType, result, context, state);
611
- const matched = result.matched_guid ? `matched GUID "${result.matched_guid}"` : "no match (new item)";
612
- console.log(`[handleHumanItemMatch] ${candidateType} "${itemName}": ${matched}`);
613
+ let resolvedType: DataItemType = candidateType;
614
+ let matched_guid = result.matched_guid;
615
+ if (matched_guid === "new") {
616
+ matched_guid = null;
617
+ } else if (matched_guid) {
618
+ const found = crossFind(matched_guid, state.getHuman());
619
+ if (!found) {
620
+ console.warn(`[handleHumanItemMatch] matched_guid "${matched_guid}" not found in human data — treating as new item`);
621
+ matched_guid = null;
622
+ } else if (found.type === "fact" && found.validated === ValidationLevel.Human) {
623
+ console.log(`[handleHumanItemMatch] Skipping locked fact "${found.name}" (human-validated)`);
624
+ return;
625
+ } else if (!(found.type === "fact" || found.type === "trait" || found.type === "topic" || found.type === "person")) {
626
+ console.warn(`[handleHumanItemMatch] matched_guid "${matched_guid}" resolved to non-human type "${found.type}" - Ignoring`);
627
+ return;
628
+ } else {
629
+ resolvedType = found.type;
630
+ context.itemName = found.name || context.itemName;
631
+ context.itemValue = found.description || context.itemValue;
632
+ }
633
+ }
634
+ result.matched_guid = matched_guid;
635
+ queueItemUpdate(resolvedType, result, context, state);
636
+ const matched = matched_guid ? `matched GUID "${matched_guid}"` : "no match (new item)";
637
+ console.log(`[handleHumanItemMatch] ${resolvedType} "${context.itemName}": ${matched}`);
613
638
  }
614
639
 
615
640
  async function handleHumanItemUpdate(response: LLMResponse, state: StateManager): Promise<void> {
@@ -632,7 +657,15 @@ async function handleHumanItemUpdate(response: LLMResponse, state: StateManager)
632
657
  }
633
658
 
634
659
  const now = new Date().toISOString();
635
- const itemId = isNewItem ? crypto.randomUUID() : (existingItemId ?? crypto.randomUUID());
660
+ const resolveItemId = (): string => {
661
+ if (isNewItem || !existingItemId) return crypto.randomUUID();
662
+ const h = state.getHuman();
663
+ const arr = candidateType === "fact" ? h.facts : candidateType === "trait" ? h.traits : candidateType === "topic" ? h.topics : h.people;
664
+ // Guard: if existingItemId isn't in the correct type array, treat as new
665
+ // (prevents cross-type ID reuse when LLM matches against a different type's UUID)
666
+ return arr.find((x: DataItemBase) => x.id === existingItemId) ? existingItemId : crypto.randomUUID();
667
+ };
668
+ const itemId = resolveItemId();
636
669
 
637
670
  const persona = state.persona_getById(personaId);
638
671
  const personaGroup = persona?.group_primary ?? null;
@@ -745,6 +778,18 @@ async function handleHumanItemUpdate(response: LLMResponse, state: StateManager)
745
778
  console.log(`[handleHumanItemUpdate] ${isNewItem ? "Created" : "Updated"} ${candidateType} "${result.name}"`);
746
779
  }
747
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
+
748
793
  async function validateAndStoreQuotes(
749
794
  candidates: Array<{ text: string; reason: string }> | undefined,
750
795
  messages: Message[],
@@ -758,22 +803,53 @@ async function validateAndStoreQuotes(
758
803
  for (const candidate of candidates) {
759
804
  let found = false;
760
805
  for (const message of messages) {
761
- const start = message.content.indexOf(candidate.text);
806
+ const msgText = getMessageText(message);
807
+ const start = msgText.indexOf(candidate.text);
762
808
  if (start !== -1) {
763
809
  const end = start + candidate.text.length;
764
810
 
811
+ // Check for ANY overlapping quote in this message (not just exact match)
765
812
  const existing = state.human_quote_getForMessage(message.id);
766
- 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
+ );
767
817
 
768
- if (existingQuote) {
769
- if (!existingQuote.data_item_ids.includes(dataItemId)) {
770
- state.human_quote_update(existingQuote.id, {
771
- data_item_ids: [...existingQuote.data_item_ids, dataItemId],
772
- });
773
- console.log(`[extraction] Linked existing quote to "${dataItemId}": "${candidate.text.slice(0, 30)}..."`);
774
- } else {
775
- 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
+ }
776
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})`);
777
853
  found = true;
778
854
  break;
779
855
  }
@@ -826,23 +902,16 @@ function applyOrValidate(
826
902
  state: StateManager,
827
903
  dataType: DataItemType,
828
904
  item: Fact | Trait | Topic | Person,
829
- personaName: string,
830
- isEi: boolean,
831
- personaGroup: string | null
905
+ _personaName: string,
906
+ _isEi: boolean,
907
+ _personaGroup: string | null
832
908
  ): void {
833
- const isGeneralGroup = !personaGroup || personaGroup.toLowerCase() === "general";
834
- const needsValidation = !isEi && isGeneralGroup;
835
-
836
909
  switch (dataType) {
837
910
  case "fact": state.human_fact_upsert(item as Fact); break;
838
911
  case "trait": state.human_trait_upsert(item as Trait); break;
839
912
  case "topic": state.human_topic_upsert(item as Topic); break;
840
913
  case "person": state.human_person_upsert(item as Person); break;
841
914
  }
842
-
843
- if (needsValidation) {
844
- queueEiValidation(state, dataType, item, personaName);
845
- }
846
915
  }
847
916
 
848
917
  const MIN_MESSAGE_COUNT_FOR_CREATE = 2;
@@ -986,52 +1055,7 @@ function handlePersonaTopicUpdate(response: LLMResponse, state: StateManager): v
986
1055
  }
987
1056
  }
988
1057
 
989
- function queueEiValidation(
990
- state: StateManager,
991
- dataType: DataItemType,
992
- item: Fact | Trait | Topic | Person,
993
- sourcePersona: string
994
- ): void {
995
- const human = state.getHuman();
996
- let existingItem: Fact | Trait | Topic | Person | undefined;
997
1058
 
998
- switch (dataType) {
999
- case "fact": existingItem = human.facts.find(f => f.id === item.id); break;
1000
- case "trait": existingItem = human.traits.find(t => t.id === item.id); break;
1001
- case "topic": existingItem = human.topics.find(t => t.id === item.id); break;
1002
- case "person": existingItem = human.people.find(p => p.id === item.id); break;
1003
- }
1004
-
1005
- const prompt = buildEiValidationPrompt({
1006
- validation_type: "cross_persona",
1007
- item_name: item.name,
1008
- data_type: dataType,
1009
- context: `Learned from conversation with ${sourcePersona}`,
1010
- source_persona: sourcePersona,
1011
- current_item: existingItem,
1012
- proposed_item: item,
1013
- });
1014
-
1015
- // Cross-persona validation is always normal priority
1016
- const priority = LLMPriority.Normal;
1017
-
1018
- state.queue_enqueue({
1019
- type: LLMRequestType.JSON,
1020
- priority,
1021
- system: prompt.system,
1022
- user: prompt.user,
1023
- next_step: NextStep.HandleEiValidation,
1024
- data: {
1025
- validationId: crypto.randomUUID(),
1026
- dataType,
1027
- itemName: item.name,
1028
- proposedItem: item,
1029
- sourcePersona,
1030
- },
1031
- });
1032
-
1033
- console.log(`[queueEiValidation] Queued ${dataType} "${item.name}" from ${sourcePersona} for Ei validation`);
1034
- }
1035
1059
 
1036
1060
  export const handlers: Record<LLMNextStep, ResponseHandler> = {
1037
1061
  handlePersonaResponse,
@@ -1049,7 +1073,6 @@ export const handlers: Record<LLMNextStep, ResponseHandler> = {
1049
1073
  handlePersonaTopicUpdate,
1050
1074
  handleHeartbeatCheck,
1051
1075
  handleEiHeartbeat,
1052
- handleEiValidation,
1053
1076
  handleOneShot,
1054
1077
  handlePersonaExpire,
1055
1078
  handlePersonaExplore,
@@ -1,6 +1,6 @@
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
- import { applyDecayToValue } from "../utils/decay.js";
3
+ import { applyDecayToValue } from "../utils/index.js";
4
4
  import {
5
5
  queueFactScan,
6
6
  queueTraitScan,
@@ -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;