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
@@ -2,6 +2,7 @@ import {
2
2
  LLMRequestType,
3
3
  LLMPriority,
4
4
  LLMNextStep,
5
+ ValidationLevel,
5
6
  RESERVED_PERSONA_NAMES,
6
7
  isReservedPersonaName,
7
8
  type LLMRequest,
@@ -35,9 +36,12 @@ import {
35
36
  buildResponsePrompt,
36
37
  buildPersonaTraitExtractionPrompt,
37
38
  buildHeartbeatCheckPrompt,
39
+ buildEiHeartbeatPrompt,
38
40
  type ResponsePromptData,
39
41
  type PersonaTraitExtractionPromptData,
40
42
  type HeartbeatCheckPromptData,
43
+ type EiHeartbeatPromptData,
44
+ type EiHeartbeatItem,
41
45
  } from "../prompts/index.js";
42
46
  import {
43
47
  orchestratePersonaGeneration,
@@ -52,6 +56,7 @@ import {
52
56
  import { EI_WELCOME_MESSAGE, EI_PERSONA_DEFINITION } from "../templates/welcome.js";
53
57
  import { getEmbeddingService, findTopK, needsEmbeddingUpdate, needsQuoteEmbeddingUpdate, computeDataItemEmbedding, computeQuoteEmbedding } from "./embedding-service.js";
54
58
  import { ContextStatus as ContextStatusEnum } from "./types.js";
59
+ import { buildChatMessageContent } from "../prompts/message-utils.js";
55
60
 
56
61
  // =============================================================================
57
62
  // EMBEDDING STRIPPING - Remove embeddings from data items before returning to FE
@@ -120,6 +125,7 @@ export class Processor {
120
125
  private currentRequest: LLMRequest | null = null;
121
126
  private isTUI = false;
122
127
  private lastOpenCodeSync = 0;
128
+ private lastDLQTrim = 0;
123
129
  private openCodeImportInProgress = false;
124
130
  private pendingConflict: StateConflictData | null = null;
125
131
 
@@ -228,7 +234,7 @@ export class Processor {
228
234
  const welcomeMessage: Message = {
229
235
  id: crypto.randomUUID(),
230
236
  role: "system",
231
- content: EI_WELCOME_MESSAGE,
237
+ verbal_response: EI_WELCOME_MESSAGE,
232
238
  timestamp: new Date().toISOString(),
233
239
  read: false,
234
240
  context_status: ContextStatusEnum.Always,
@@ -330,31 +336,36 @@ export class Processor {
330
336
  await this.checkScheduledTasks();
331
337
 
332
338
  if (this.queueProcessor.getState() === "idle") {
333
- const request = this.stateManager.queue_peekHighest();
334
- if (request) {
335
- const personaId = request.data.personaId as string | undefined;
336
- const personaDisplayName = request.data.personaDisplayName as string | undefined;
337
- const personaSuffix = personaDisplayName ? ` [${personaDisplayName}]` : "";
338
- console.log(`[Processor ${this.instanceId}] processing request: ${request.next_step}${personaSuffix}`);
339
- this.currentRequest = request;
340
-
341
- if (personaId && request.next_step === LLMNextStep.HandlePersonaResponse) {
342
- 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");
343
368
  }
344
-
345
- this.queueProcessor.start(request, async (response) => {
346
- this.currentRequest = null;
347
- await this.handleResponse(response);
348
- const nextState = this.stateManager.queue_isPaused() ? "paused" : "idle";
349
- // the processor state is set in the caller, so this needs a bit of delay
350
- setTimeout(() => this.interface.onQueueStateChanged?.(nextState), 0);
351
- }, {
352
- accounts: this.stateManager.getHuman().settings?.accounts,
353
- messageFetcher: (pName) => this.fetchMessagesForLLM(pName),
354
- rawMessageFetcher: (pName) => this.stateManager.messages_get(pName),
355
- });
356
-
357
- this.interface.onQueueStateChanged?.("busy");
358
369
  }
359
370
  }
360
371
 
@@ -369,7 +380,7 @@ export class Processor {
369
380
 
370
381
  const human = this.stateManager.getHuman();
371
382
 
372
- if (this.isTUI && human.settings?.opencode?.integration) {
383
+ if (this.isTUI && human.settings?.opencode?.integration && this.stateManager.queue_length() === 0) {
373
384
  await this.checkAndSyncOpenCode(human, now);
374
385
  }
375
386
 
@@ -403,6 +414,15 @@ export class Processor {
403
414
  }
404
415
  }
405
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
+ }
406
426
  }
407
427
 
408
428
  private async checkAndSyncOpenCode(human: HumanEntity, now: number): Promise<void> {
@@ -434,12 +454,10 @@ export class Processor {
434
454
  },
435
455
  });
436
456
 
437
- const since = lastSync > 0 ? new Date(lastSync) : new Date(0);
438
-
439
457
  this.openCodeImportInProgress = true;
440
458
  import("../integrations/opencode/importer.js")
441
- .then(({ importOpenCodeSessions }) =>
442
- importOpenCodeSessions(since, {
459
+ .then(({ importOpenCodeSessions }) =>
460
+ importOpenCodeSessions({
443
461
  stateManager: this.stateManager,
444
462
  interface: this.interface,
445
463
  })
@@ -449,7 +467,7 @@ export class Processor {
449
467
  console.log(
450
468
  `[Processor] OpenCode sync complete: ${result.sessionsProcessed} sessions, ` +
451
469
  `${result.topicsCreated} topics created, ${result.messagesImported} messages imported, ` +
452
- `${result.topicUpdatesQueued} topic updates queued`
470
+ `${result.extractionScansQueued} extraction scans queued`
453
471
  );
454
472
  }
455
473
  })
@@ -482,29 +500,36 @@ export class Processor {
482
500
  contextWindowHours
483
501
  );
484
502
 
485
- return filteredHistory.map((m) => ({
486
- role: m.role === "human" ? "user" : "assistant",
487
- content: m.content,
488
- })) 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
+ }, []);
489
514
  }
490
515
 
491
516
  private async queueHeartbeatCheck(personaId: string): Promise<void> {
492
517
  const persona = this.stateManager.persona_getById(personaId);
493
518
  if (!persona) return;
494
-
495
519
  this.stateManager.persona_update(personaId, { last_heartbeat: new Date().toISOString() });
496
-
497
520
  const human = this.stateManager.getHuman();
498
521
  const history = this.stateManager.messages_get(personaId);
499
- const filteredHuman = await this.filterHumanDataByVisibility(human, persona);
522
+ if (personaId === "ei") {
523
+ await this.queueEiHeartbeat(human, history);
524
+ return;
525
+ }
500
526
 
527
+ const filteredHuman = await this.filterHumanDataByVisibility(human, persona);
501
528
  const inactiveDays = persona.last_activity
502
529
  ? Math.floor((Date.now() - new Date(persona.last_activity).getTime()) / (1000 * 60 * 60 * 24))
503
530
  : 0;
504
-
505
531
  const sortByEngagementGap = <T extends { exposure_desired: number; exposure_current: number }>(items: T[]): T[] =>
506
532
  [...items].sort((a, b) => (b.exposure_desired - b.exposure_current) - (a.exposure_desired - a.exposure_current));
507
-
508
533
  const promptData: HeartbeatCheckPromptData = {
509
534
  persona: {
510
535
  name: persona.display_name,
@@ -532,6 +557,118 @@ export class Processor {
532
557
  });
533
558
  }
534
559
 
560
+ private async queueEiHeartbeat(human: HumanEntity, history: import("./types.js").Message[]): Promise<void> {
561
+ const now = Date.now();
562
+ const engagementGapThreshold = 0.2;
563
+ const cooldownMs = 7 * 24 * 60 * 60 * 1000;
564
+ const personas = this.stateManager.persona_getAll();
565
+ const items: EiHeartbeatItem[] = [];
566
+
567
+ const unverifiedFacts = human.facts
568
+ .filter(f => f.validated === ValidationLevel.None && f.learned_by !== "Ei")
569
+ .slice(0, 5);
570
+ for (const fact of unverifiedFacts) {
571
+ const quote = human.quotes.find(q => q.data_item_ids.includes(fact.id));
572
+ items.push({
573
+ id: fact.id,
574
+ type: "Fact Check",
575
+ name: fact.name,
576
+ description: fact.description,
577
+ quote: quote?.text,
578
+ });
579
+ }
580
+
581
+ const underEngagedPeople = human.people
582
+ .filter(p =>
583
+ (p.exposure_desired - p.exposure_current) > engagementGapThreshold &&
584
+ (!p.last_ei_asked || now - new Date(p.last_ei_asked).getTime() > cooldownMs)
585
+ )
586
+ .sort((a, b) => (b.exposure_desired - b.exposure_current) - (a.exposure_desired - a.exposure_current))
587
+ .slice(0, 5);
588
+ for (const person of underEngagedPeople) {
589
+ const gap = Math.round((person.exposure_desired - person.exposure_current) * 100);
590
+ const quote = human.quotes.find(q => q.data_item_ids.includes(person.id));
591
+ items.push({
592
+ id: person.id,
593
+ type: "Low-Engagement Person",
594
+ engagement_delta: `${gap}%`,
595
+ relationship: person.relationship,
596
+ name: person.name,
597
+ description: person.description,
598
+ quote: quote?.text,
599
+ });
600
+ }
601
+
602
+ const underEngagedTopics = human.topics
603
+ .filter(t =>
604
+ (t.exposure_desired - t.exposure_current) > engagementGapThreshold &&
605
+ (!t.last_ei_asked || now - new Date(t.last_ei_asked).getTime() > cooldownMs)
606
+ )
607
+ .sort((a, b) => (b.exposure_desired - b.exposure_current) - (a.exposure_desired - a.exposure_current))
608
+ .slice(0, 5);
609
+ for (const topic of underEngagedTopics) {
610
+ const gap = Math.round((topic.exposure_desired - topic.exposure_current) * 100);
611
+ const quote = human.quotes.find(q => q.data_item_ids.includes(topic.id));
612
+ items.push({
613
+ id: topic.id,
614
+ type: "Low-Engagement Topic",
615
+ engagement_delta: `${gap}%`,
616
+ name: topic.name,
617
+ description: topic.description,
618
+ quote: quote?.text,
619
+ });
620
+ }
621
+
622
+ const activePersonas = personas
623
+ .filter(p => !p.is_archived && !p.is_paused && p.id !== "ei")
624
+ .map(p => {
625
+ const msgs = this.stateManager.messages_get(p.id);
626
+ const lastHuman = [...msgs].reverse().find(m => m.role === "human");
627
+ const lastTs = lastHuman?.timestamp ? new Date(lastHuman.timestamp).getTime() : 0;
628
+ return { persona: p, lastHumanTs: lastTs };
629
+ })
630
+ .filter(({ lastHumanTs }) => {
631
+ const daysSince = (now - lastHumanTs) / (1000 * 60 * 60 * 24);
632
+ return daysSince >= 3;
633
+ })
634
+ .sort((a, b) => a.lastHumanTs - b.lastHumanTs)
635
+ .slice(0, 3);
636
+ for (const { persona: p, lastHumanTs } of activePersonas) {
637
+ const daysSince = lastHumanTs > 0
638
+ ? Math.floor((now - lastHumanTs) / (1000 * 60 * 60 * 24))
639
+ : 999;
640
+ items.push({
641
+ id: p.id,
642
+ type: "Inactive Persona",
643
+ name: p.display_name,
644
+ short_description: p.short_description,
645
+ days_inactive: daysSince,
646
+ });
647
+ }
648
+
649
+ if (items.length === 0) {
650
+ console.log("[queueEiHeartbeat] No items to address, skipping");
651
+ return;
652
+ }
653
+
654
+ const promptData: EiHeartbeatPromptData = {
655
+ items,
656
+ recent_history: history.slice(-10),
657
+ };
658
+
659
+ const prompt = buildEiHeartbeatPrompt(promptData);
660
+
661
+ this.stateManager.queue_enqueue({
662
+ type: LLMRequestType.JSON,
663
+ priority: LLMPriority.Low,
664
+ system: prompt.system,
665
+ user: prompt.user,
666
+ next_step: LLMNextStep.HandleEiHeartbeat,
667
+ model: this.getModelForPersona("ei"),
668
+ data: { personaId: "ei", isTUI: this.isTUI },
669
+ });
670
+ }
671
+
535
672
 
536
673
  private classifyLLMError(error: string): string {
537
674
  const match = error.match(/\((\d{3})\)/);
@@ -627,9 +764,7 @@ export class Processor {
627
764
  }
628
765
  }
629
766
 
630
- if (response.request.next_step === LLMNextStep.HandleEiValidation) {
631
- this.interface.onHumanUpdated?.();
632
- }
767
+
633
768
 
634
769
  if (response.request.next_step === LLMNextStep.HandleHumanItemUpdate) {
635
770
  this.interface.onHumanUpdated?.();
@@ -813,23 +948,16 @@ export class Processor {
813
948
  async recallPendingMessages(personaId: string): Promise<string> {
814
949
  const persona = this.stateManager.persona_getById(personaId);
815
950
  if (!persona) return "";
816
-
817
951
  this.clearPendingRequestsFor(personaId);
818
- this.stateManager.queue_pause();
819
-
820
952
  const messages = this.stateManager.messages_get(personaId);
821
953
  const pendingIds = messages
822
954
  .filter(m => m.role === "human" && !m.read)
823
955
  .map(m => m.id);
824
-
825
956
  if (pendingIds.length === 0) return "";
826
-
827
957
  const removed = this.stateManager.messages_remove(personaId, pendingIds);
828
- const recalledContent = removed.map(m => m.content).join("\n\n");
829
-
958
+ const recalledContent = removed.map(m => m.verbal_response ?? '').join("\n\n");
830
959
  this.interface.onMessageAdded?.(personaId);
831
960
  this.interface.onMessageRecalled?.(personaId, recalledContent);
832
-
833
961
  return recalledContent;
834
962
  }
835
963
 
@@ -848,7 +976,7 @@ export class Processor {
848
976
  const message: Message = {
849
977
  id: crypto.randomUUID(),
850
978
  role: "human",
851
- content,
979
+ verbal_response: content,
852
980
  timestamp: new Date().toISOString(),
853
981
  read: false,
854
982
  context_status: "default" as ContextStatus,
@@ -987,13 +1115,40 @@ export class Processor {
987
1115
  ): Promise<ResponsePromptData["human"]> {
988
1116
  const DEFAULT_GROUP = "General";
989
1117
  const QUOTE_LIMIT = 10;
1118
+ const DATA_ITEM_LIMIT = 15;
990
1119
  const SIMILARITY_THRESHOLD = 0.3;
1120
+ // Generic relevance selector for embedding-capable items.
1121
+ // Falls back to returning all items when no message/embeddings are available.
1122
+ const selectRelevantItems = async <T extends { id: string; embedding?: number[] }>(
1123
+ items: T[],
1124
+ limit: number
1125
+ ): Promise<T[]> => {
1126
+ if (items.length === 0) return [];
1127
+
1128
+ const withEmbeddings = items.filter(i => i.embedding?.length);
1129
+
1130
+ if (currentMessage && withEmbeddings.length > 0) {
1131
+ try {
1132
+ const embeddingService = getEmbeddingService();
1133
+ const queryVector = await embeddingService.embed(currentMessage);
1134
+ const results = findTopK(queryVector, withEmbeddings, limit);
1135
+ const relevant = results
1136
+ .filter(({ similarity }) => similarity >= SIMILARITY_THRESHOLD)
1137
+ .map(({ item }) => item);
1138
+
1139
+ if (relevant.length > 0) return relevant;
1140
+ } catch (err) {
1141
+ console.warn("[filterHumanDataByVisibility] Embedding search failed:", err);
1142
+ }
1143
+ }
991
1144
 
1145
+ // Fallback: return all items (caller may apply its own limit)
1146
+ return items;
1147
+ };
992
1148
  const selectRelevantQuotes = async (quotes: Quote[]): Promise<Quote[]> => {
993
1149
  if (quotes.length === 0) return [];
994
-
995
1150
  const withEmbeddings = quotes.filter(q => q.embedding?.length);
996
-
1151
+
997
1152
  if (currentMessage && withEmbeddings.length > 0) {
998
1153
  try {
999
1154
  const embeddingService = getEmbeddingService();
@@ -1002,35 +1157,31 @@ export class Processor {
1002
1157
  const relevant = results
1003
1158
  .filter(({ similarity }) => similarity >= SIMILARITY_THRESHOLD)
1004
1159
  .map(({ item }) => item);
1005
-
1160
+
1006
1161
  if (relevant.length > 0) return relevant;
1007
1162
  } catch (err) {
1008
1163
  console.warn("[filterHumanDataByVisibility] Embedding search failed:", err);
1009
1164
  }
1010
1165
  }
1011
-
1012
1166
  return [...quotes]
1013
1167
  .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
1014
1168
  .slice(0, QUOTE_LIMIT);
1015
1169
  };
1016
-
1017
1170
  if (persona.id === "ei") {
1018
- const relevantQuotes = await selectRelevantQuotes(human.quotes ?? []);
1019
- return {
1020
- facts: human.facts,
1021
- traits: human.traits,
1022
- topics: human.topics,
1023
- people: human.people,
1024
- quotes: relevantQuotes,
1025
- };
1171
+ const [facts, traits, topics, people, quotes] = await Promise.all([
1172
+ selectRelevantItems(human.facts, DATA_ITEM_LIMIT),
1173
+ selectRelevantItems(human.traits, DATA_ITEM_LIMIT),
1174
+ selectRelevantItems(human.topics, DATA_ITEM_LIMIT),
1175
+ selectRelevantItems(human.people, DATA_ITEM_LIMIT),
1176
+ selectRelevantQuotes(human.quotes ?? []),
1177
+ ]);
1178
+ return { facts, traits, topics, people, quotes };
1026
1179
  }
1027
-
1028
1180
  const visibleGroups = new Set<string>();
1029
1181
  if (persona.group_primary) {
1030
1182
  visibleGroups.add(persona.group_primary);
1031
1183
  }
1032
1184
  (persona.groups_visible ?? []).forEach((g) => visibleGroups.add(g));
1033
-
1034
1185
  const filterByGroup = <T extends DataItemBase>(items: T[]): T[] => {
1035
1186
  return items.filter((item) => {
1036
1187
  const itemGroups = item.persona_groups ?? [];
@@ -1038,21 +1189,20 @@ export class Processor {
1038
1189
  return effectiveGroups.some((g) => visibleGroups.has(g));
1039
1190
  });
1040
1191
  };
1041
-
1042
1192
  const groupFilteredQuotes = (human.quotes ?? []).filter((q) => {
1043
1193
  const effectiveGroups = q.persona_groups.length === 0 ? [DEFAULT_GROUP] : q.persona_groups;
1044
1194
  return effectiveGroups.some((g) => visibleGroups.has(g));
1045
1195
  });
1046
1196
 
1047
- const relevantQuotes = await selectRelevantQuotes(groupFilteredQuotes);
1197
+ const [facts, traits, topics, people, quotes] = await Promise.all([
1198
+ selectRelevantItems(filterByGroup(human.facts), DATA_ITEM_LIMIT),
1199
+ selectRelevantItems(filterByGroup(human.traits), DATA_ITEM_LIMIT),
1200
+ selectRelevantItems(filterByGroup(human.topics), DATA_ITEM_LIMIT),
1201
+ selectRelevantItems(filterByGroup(human.people), DATA_ITEM_LIMIT),
1202
+ selectRelevantQuotes(groupFilteredQuotes),
1203
+ ]);
1048
1204
 
1049
- return {
1050
- facts: filterByGroup(human.facts),
1051
- traits: filterByGroup(human.traits),
1052
- topics: filterByGroup(human.topics),
1053
- people: filterByGroup(human.people),
1054
- quotes: relevantQuotes,
1055
- };
1205
+ return { facts, traits, topics, people, quotes };
1056
1206
  }
1057
1207
 
1058
1208
  private getVisiblePersonas(
@@ -1342,13 +1492,31 @@ export class Processor {
1342
1492
  return {
1343
1493
  state: this.stateManager.queue_isPaused()
1344
1494
  ? "paused"
1345
- : this.queueProcessor.getState() === "busy"
1495
+ : this.stateManager.queue_hasProcessingItem()
1346
1496
  ? "busy"
1347
1497
  : "idle",
1348
1498
  pending_count: this.stateManager.queue_length(),
1499
+ dlq_count: this.stateManager.queue_dlqLength(),
1349
1500
  };
1350
1501
  }
1351
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
+
1352
1520
  async clearQueue(): Promise<number> {
1353
1521
  this.queueProcessor.abort();
1354
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
- }
@@ -45,6 +45,10 @@ export class PersistenceState {
45
45
  return this.loadedExistingData;
46
46
  }
47
47
 
48
+ markExistingData(): void {
49
+ this.loadedExistingData = true;
50
+ }
51
+
48
52
  async flush(): Promise<void> {
49
53
  if (this.saveTimeout) {
50
54
  clearTimeout(this.saveTimeout);
@@ -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
  }