ei-tui 0.5.4 → 0.6.1

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 (86) hide show
  1. package/package.json +1 -1
  2. package/src/core/constants/built-in-identifier-types.ts +24 -0
  3. package/src/core/embedding-service.ts +24 -1
  4. package/src/core/handlers/dedup.ts +34 -4
  5. package/src/core/handlers/heartbeat.ts +16 -0
  6. package/src/core/handlers/human-extraction.ts +201 -7
  7. package/src/core/handlers/human-matching.ts +71 -22
  8. package/src/core/handlers/index.ts +52 -14
  9. package/src/core/handlers/persona-generation.ts +2 -0
  10. package/src/core/handlers/persona-response.ts +37 -22
  11. package/src/core/handlers/persona-topics.ts +35 -271
  12. package/src/core/handlers/rewrite.ts +3 -0
  13. package/src/core/handlers/rooms.ts +41 -20
  14. package/src/core/handlers/utils.ts +10 -8
  15. package/src/core/heartbeat-manager.ts +60 -2
  16. package/src/core/llm-client.ts +1 -1
  17. package/src/core/message-manager.ts +3 -2
  18. package/src/core/orchestrators/ceremony.ts +54 -144
  19. package/src/core/orchestrators/dedup-phase.ts +0 -199
  20. package/src/core/orchestrators/extraction-chunker.ts +8 -3
  21. package/src/core/orchestrators/human-extraction.ts +37 -85
  22. package/src/core/orchestrators/index.ts +4 -8
  23. package/src/core/orchestrators/person-migration.ts +55 -0
  24. package/src/core/orchestrators/persona-topics.ts +64 -89
  25. package/src/core/orchestrators/room-extraction.ts +34 -0
  26. package/src/core/persona-manager.ts +21 -2
  27. package/src/core/personas/opencode-agent.ts +1 -0
  28. package/src/core/processor.ts +51 -14
  29. package/src/core/prompt-context-builder.ts +38 -5
  30. package/src/core/queue-processor.ts +4 -2
  31. package/src/core/room-manager.ts +6 -7
  32. package/src/core/state/human.ts +6 -0
  33. package/src/core/state/personas.ts +35 -10
  34. package/src/core/state/rooms.ts +21 -0
  35. package/src/core/state-manager.ts +61 -0
  36. package/src/core/types/data-items.ts +12 -0
  37. package/src/core/types/entities.ts +3 -0
  38. package/src/core/types/enums.ts +2 -7
  39. package/src/core/types/llm.ts +2 -0
  40. package/src/core/types/rooms.ts +2 -0
  41. package/src/core/utils/identifier-utils.ts +19 -0
  42. package/src/core/utils/index.ts +2 -1
  43. package/src/core/utils/levenshtein.ts +18 -0
  44. package/src/integrations/claude-code/importer.ts +1 -0
  45. package/src/integrations/cursor/importer.ts +1 -0
  46. package/src/prompts/ceremony/index.ts +1 -0
  47. package/src/prompts/ceremony/person-migration.ts +77 -0
  48. package/src/prompts/ceremony/rewrite.ts +1 -1
  49. package/src/prompts/ceremony/user-dedup.ts +15 -1
  50. package/src/prompts/heartbeat/check.ts +28 -12
  51. package/src/prompts/heartbeat/ei.ts +2 -0
  52. package/src/prompts/heartbeat/types.ts +12 -0
  53. package/src/prompts/human/index.ts +0 -2
  54. package/src/prompts/human/person-scan.ts +58 -14
  55. package/src/prompts/human/person-update.ts +171 -96
  56. package/src/prompts/human/topic-update.ts +1 -1
  57. package/src/prompts/human/types.ts +5 -1
  58. package/src/prompts/index.ts +3 -10
  59. package/src/prompts/message-utils.ts +9 -23
  60. package/src/prompts/persona/index.ts +3 -10
  61. package/src/prompts/persona/topics-rate.ts +95 -0
  62. package/src/prompts/persona/types.ts +8 -48
  63. package/src/prompts/response/index.ts +3 -7
  64. package/src/prompts/response/sections.ts +7 -57
  65. package/src/prompts/room/index.ts +1 -1
  66. package/src/prompts/room/sections.ts +8 -31
  67. package/tui/src/commands/me.tsx +14 -7
  68. package/tui/src/commands/persona.tsx +120 -83
  69. package/tui/src/components/MessageList.tsx +9 -4
  70. package/tui/src/components/RoomMessageList.tsx +10 -5
  71. package/tui/src/context/keyboard.tsx +2 -2
  72. package/tui/src/util/cyp-editor.tsx +13 -8
  73. package/tui/src/util/yaml-context.ts +66 -0
  74. package/tui/src/util/yaml-human.ts +274 -0
  75. package/tui/src/util/yaml-persona.ts +479 -0
  76. package/tui/src/util/yaml-provider.ts +215 -0
  77. package/tui/src/util/yaml-queue.ts +81 -0
  78. package/tui/src/util/yaml-quotes.ts +46 -0
  79. package/tui/src/util/yaml-serializers.ts +9 -1417
  80. package/tui/src/util/yaml-settings.ts +223 -0
  81. package/tui/src/util/yaml-shared.ts +32 -0
  82. package/tui/src/util/yaml-toolkit.ts +55 -0
  83. package/src/prompts/human/person-match.ts +0 -65
  84. package/src/prompts/persona/topics-match.ts +0 -70
  85. package/src/prompts/persona/topics-scan.ts +0 -98
  86. package/src/prompts/persona/topics-update.ts +0 -154
@@ -1,4 +1,5 @@
1
1
  import { LLMRequestType, LLMPriority, LLMNextStep, type Message, type Topic, type Person } from "../types.js";
2
+ import type { PersonIdentifier } from "../types/data-items.js";
2
3
  import type { StateManager } from "../state-manager.js";
3
4
  import {
4
5
  buildFactFindPrompt,
@@ -6,16 +7,14 @@ import {
6
7
  buildHumanPersonScanPrompt,
7
8
  buildTopicMatchPrompt,
8
9
  buildTopicUpdatePrompt,
9
- buildPersonMatchPrompt,
10
10
  buildPersonUpdatePrompt,
11
11
  buildEventScanPrompt,
12
12
  type TopicScanCandidate,
13
- type PersonScanCandidate,
14
13
  type ItemMatchResult,
15
14
  type ParticipantContext,
16
15
  } from "../../prompts/human/index.js";
17
16
  import { chunkExtractionContext } from "./extraction-chunker.js";
18
- import { getEmbeddingService, findTopK, getTopicEmbeddingText, getPersonEmbeddingText } from "../embedding-service.js";
17
+ import { getEmbeddingService, findTopK, getTopicEmbeddingText } from "../embedding-service.js";
19
18
  import { resolveTokenLimit } from "../llm-client.js";
20
19
  import { BUILT_IN_FACT_NAMES } from "../constants/built-in-facts.js";
21
20
  import { buildEventWindows } from "../utils/event-windows.js";
@@ -192,11 +191,20 @@ export function queuePersonScan(context: ExtractionContext, state: StateManager,
192
191
  state.messages_markExtracted(chunk.personaId, chunk.messages_analyze.map(m => m.id), "p");
193
192
  }
194
193
 
194
+ const humanForScan = state.getHuman();
195
+ const userIdentifierTypesForScan = [...new Set(
196
+ humanForScan.people
197
+ .flatMap(p => (p.identifiers ?? []).map(i => i.type))
198
+ .filter(Boolean)
199
+ )];
200
+
195
201
  for (const chunk of chunks) {
196
202
  const prompt = buildHumanPersonScanPrompt({
197
203
  persona_name: chunk.personaDisplayName,
198
204
  messages_context: chunk.messages_context,
199
205
  messages_analyze: chunk.messages_analyze,
206
+ participant_context: buildParticipantContext(context.personaId, state),
207
+ known_identifier_types: userIdentifierTypesForScan,
200
208
  });
201
209
 
202
210
  state.queue_enqueue({
@@ -364,88 +372,6 @@ export async function queueTopicMatch(
364
372
  });
365
373
  }
366
374
 
367
- /**
368
- * Queue a person match request using embedding-based similarity (people only).
369
- */
370
- export async function queuePersonMatch(
371
- candidate: PersonScanCandidate,
372
- context: ExtractionContext,
373
- state: StateManager,
374
- extractionModel?: string
375
- ): Promise<void> {
376
- const human = state.getHuman();
377
-
378
- const peopleWithEmbeddings = human.people.filter(p => p.embedding && p.embedding.length > 0);
379
-
380
- let topKItems: Array<{ id: string; name: string; description: string; relationship?: string }> = [];
381
-
382
- if (peopleWithEmbeddings.length > 0) {
383
- try {
384
- const embeddingService = getEmbeddingService();
385
- const candidateText = getPersonEmbeddingText({
386
- name: candidate.name,
387
- relationship: candidate.relationship,
388
- description: candidate.description,
389
- });
390
- const candidateVector = await embeddingService.embed(candidateText);
391
-
392
- const topK = findTopK(candidateVector, peopleWithEmbeddings, EMBEDDING_TOP_K);
393
- topKItems = topK
394
- .filter(({ similarity }) => similarity >= EMBEDDING_MIN_SIMILARITY)
395
- .map(({ item }) => ({
396
- id: item.id,
397
- name: item.name,
398
- description: item.description,
399
- relationship: item.relationship,
400
- }));
401
-
402
- console.log(`[queuePersonMatch] Embedding search: ${peopleWithEmbeddings.length} people → ${topKItems.length} candidates`);
403
- if (topKItems.length > 0) state.embedding_setWarning(false);
404
- } catch (err) {
405
- console.error(`[queuePersonMatch] Embedding search failed, falling back to recent people:`, err);
406
- state.embedding_setWarning(true);
407
- }
408
- }
409
-
410
- if (topKItems.length === 0) {
411
- const sorted = [...human.people].sort((a, b) => {
412
- const aDate = a.last_mentioned ?? a.last_updated;
413
- const bDate = b.last_mentioned ?? b.last_updated;
414
- return bDate.localeCompare(aDate);
415
- });
416
- topKItems = sorted.slice(0, EMBEDDING_TOP_K).map(p => ({
417
- id: p.id,
418
- name: p.name,
419
- description: p.description,
420
- relationship: p.relationship,
421
- }));
422
- console.log(`[queuePersonMatch] No embedding matches, using ${topKItems.length} most-recent people`);
423
- }
424
-
425
- const prompt = buildPersonMatchPrompt({
426
- candidate_name: candidate.name,
427
- candidate_description: candidate.description,
428
- candidate_relationship: candidate.relationship,
429
- existing_people: topKItems,
430
- });
431
-
432
- state.queue_enqueue({
433
- type: LLMRequestType.JSON,
434
- priority: LLMPriority.Normal,
435
- model: extractionModel,
436
- system: prompt.system,
437
- user: prompt.user,
438
- next_step: LLMNextStep.HandlePersonMatch,
439
- data: {
440
- ...context,
441
- candidateName: candidate.name,
442
- candidateDescription: candidate.description,
443
- candidateRelationship: candidate.relationship,
444
- extraction_model: extractionModel,
445
- },
446
- });
447
- }
448
-
449
375
  export function queueTopicUpdate(
450
376
  matchResult: ItemMatchResult,
451
377
  context: ExtractionContext & {
@@ -592,6 +518,7 @@ export function queuePersonUpdate(
592
518
  candidateName: string;
593
519
  candidateDescription: string;
594
520
  candidateRelationship: string;
521
+ candidateIdentifiers?: PersonIdentifier[];
595
522
  extraction_model?: string;
596
523
  },
597
524
  state: StateManager
@@ -605,11 +532,32 @@ export function queuePersonUpdate(
605
532
  existingItem = human.people.find(p => p.id === matchedGuid) ?? null;
606
533
  }
607
534
 
535
+ const candidateIdentifiers = context.candidateIdentifiers ?? [];
536
+
537
+ if (!isNewItem && existingItem && candidateIdentifiers.length > 0) {
538
+ const merged = [...(existingItem.identifiers ?? [])];
539
+ for (const ci of candidateIdentifiers) {
540
+ if (!merged.some(ei => ei.value === ci.value)) {
541
+ merged.push(ci);
542
+ }
543
+ }
544
+ existingItem = { ...existingItem, identifiers: merged };
545
+ state.human_person_upsert(existingItem);
546
+ }
547
+
548
+ const userIdentifierTypes = [...new Set(
549
+ human.people
550
+ .flatMap(p => (p.identifiers ?? []).map(i => i.type))
551
+ .filter(Boolean)
552
+ )];
553
+
608
554
  const extractionOptions: ExtractionOptions = { extraction_model: context.extraction_model };
609
555
  const { chunks } = chunkExtractionContext(context, getExtractionMaxTokens(state, extractionOptions));
610
556
 
611
557
  if (chunks.length === 0) return 0;
612
558
 
559
+ const primaryPersonaIdForUpdate = context.personaId.split("|")[0];
560
+
613
561
  for (const chunk of chunks) {
614
562
  const prompt = buildPersonUpdatePrompt({
615
563
  existing_item: existingItem,
@@ -619,6 +567,8 @@ export function queuePersonUpdate(
619
567
  messages_context: chunk.messages_context,
620
568
  messages_analyze: chunk.messages_analyze,
621
569
  persona_name: chunk.personaDisplayName,
570
+ participant_context: buildParticipantContext(primaryPersonaIdForUpdate, state),
571
+ known_identifier_types: userIdentifierTypes,
622
572
  });
623
573
 
624
574
  state.queue_enqueue({
@@ -634,7 +584,9 @@ export function queuePersonUpdate(
634
584
  roomId: context.roomId,
635
585
  isNewItem,
636
586
  existingItemId: existingItem?.id,
587
+ candidateName: context.candidateName,
637
588
  candidateRelationship: context.candidateRelationship,
589
+ candidateIdentifiers: isNewItem ? candidateIdentifiers : undefined,
638
590
  analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
639
591
  },
640
592
  });
@@ -6,7 +6,6 @@ export {
6
6
  queueAllScans,
7
7
  queueTopicMatch,
8
8
  queueTopicUpdate,
9
- queuePersonMatch,
10
9
  queuePersonUpdate,
11
10
  queueEventSummary,
12
11
  type ExtractionContext,
@@ -17,16 +16,13 @@ export {
17
16
  startCeremony,
18
17
  handleCeremonyProgress,
19
18
  prunePersonaMessages,
20
- queueExpirePhase,
21
- queueExplorePhase,
22
- queueDescriptionCheck,
23
19
  runHumanCeremony,
24
20
  } from "./ceremony.js";
25
- export { queueDedupPhase, queueUserDedupRequest } from "./dedup-phase.js";
21
+ export { queueUserDedupRequest } from "./dedup-phase.js";
22
+ export { queuePersonMigration } from "./person-migration.js";
26
23
  export {
27
- queuePersonaTopicScan,
28
- queuePersonaTopicMatch,
29
- queuePersonaTopicUpdate,
24
+ queuePersonaTopicRating,
30
25
  type PersonaTopicContext,
26
+ type PersonaTopicOptions,
31
27
  } from "./persona-topics.js";
32
28
  export { queueRoomCapture, queuePersonaCapture, checkAndQueueRoomExtraction } from "./room-extraction.js";
@@ -0,0 +1,55 @@
1
+ import { LLMRequestType, LLMPriority, LLMNextStep } from "../types.js";
2
+ import type { StateManager } from "../state-manager.js";
3
+ import { buildPersonMigrationPrompt } from "../../prompts/ceremony/index.js";
4
+
5
+ export function queuePersonMigration(state: StateManager): void {
6
+ const human = state.getHuman();
7
+
8
+ if (human.settings?.people_migration_complete) {
9
+ console.log("[PersonMigration] Migration complete flag set — skipping");
10
+ return;
11
+ }
12
+
13
+ const unmigrated = human.people.filter(p => !p.identifiers || p.identifiers.length === 0);
14
+
15
+ if (unmigrated.length === 0) {
16
+ console.log("[PersonMigration] All Person records have identifiers — marking migration complete");
17
+ state.setHuman({
18
+ ...human,
19
+ settings: {
20
+ ...human.settings,
21
+ people_migration_complete: true,
22
+ },
23
+ });
24
+ return;
25
+ }
26
+
27
+ console.log(`[PersonMigration] Queuing migration for ${unmigrated.length} Person record(s)`);
28
+
29
+ const rewriteModel = human.settings?.rewrite_model;
30
+
31
+ for (const person of unmigrated) {
32
+ const prompt = buildPersonMigrationPrompt({
33
+ person: {
34
+ name: person.name,
35
+ description: person.description,
36
+ relationship: person.relationship,
37
+ },
38
+ });
39
+
40
+ state.queue_enqueue({
41
+ type: LLMRequestType.JSON,
42
+ priority: LLMPriority.Normal,
43
+ system: prompt.system,
44
+ user: prompt.user,
45
+ next_step: LLMNextStep.HandlePersonIdentifierMigration,
46
+ ...(rewriteModel ? { model: rewriteModel } : {}),
47
+ data: {
48
+ person_id: person.id,
49
+ ceremony_progress: 1,
50
+ },
51
+ });
52
+ }
53
+
54
+ console.log(`[PersonMigration] Queued ${unmigrated.length} migration request(s)`);
55
+ }
@@ -1,118 +1,93 @@
1
1
  import { LLMRequestType, LLMPriority, LLMNextStep, type Message, type PersonaTopic } from "../types.js";
2
2
  import type { StateManager } from "../state-manager.js";
3
3
  import {
4
- buildPersonaTopicScanPrompt,
5
- buildPersonaTopicMatchPrompt,
6
- buildPersonaTopicUpdatePrompt,
7
- type PersonaTopicScanCandidate,
8
- type PersonaTopicMatchResult,
4
+ buildPersonaTopicRatingPrompt,
9
5
  } from "../../prompts/persona/index.js";
6
+ import { chunkExtractionContext } from "./extraction-chunker.js";
7
+ import { resolveTokenLimit } from "../llm-client.js";
10
8
 
11
9
  export interface PersonaTopicContext {
12
10
  personaId: string;
13
11
  personaDisplayName: string;
14
12
  messages_context: Message[];
15
13
  messages_analyze: Message[];
14
+ topics: PersonaTopic[];
16
15
  }
17
16
 
18
- function getAnalyzeFromTimestamp(context: PersonaTopicContext): string | null {
19
- if (context.messages_analyze.length === 0) return null;
20
- return context.messages_analyze[0].timestamp;
17
+ export interface PersonaTopicOptions {
18
+ ceremony_progress?: number;
19
+ roomId?: string;
21
20
  }
22
21
 
23
- export function queuePersonaTopicScan(context: PersonaTopicContext, state: StateManager): void {
24
- const prompt = buildPersonaTopicScanPrompt({
25
- persona_name: context.personaDisplayName,
26
- messages_context: context.messages_context,
27
- messages_analyze: context.messages_analyze,
28
- });
22
+ const EXTRACTION_BUDGET_RATIO = 0.75;
23
+ const MIN_EXTRACTION_TOKENS = 10000;
29
24
 
30
- state.queue_enqueue({
31
- type: LLMRequestType.JSON,
32
- priority: LLMPriority.Low,
33
- system: prompt.system,
34
- user: prompt.user,
35
- next_step: LLMNextStep.HandlePersonaTopicScan,
36
- data: {
37
- personaId: context.personaId,
38
- personaDisplayName: context.personaDisplayName,
39
- analyze_from_timestamp: getAnalyzeFromTimestamp(context),
40
- },
41
- });
25
+ function getExtractionMaxTokens(state: StateManager): number {
26
+ const human = state.getHuman();
27
+ const modelForTokenLimit = human.settings?.default_model;
28
+ const tokenLimit = resolveTokenLimit(modelForTokenLimit, human.settings?.accounts);
29
+ return Math.max(MIN_EXTRACTION_TOKENS, Math.floor(tokenLimit * EXTRACTION_BUDGET_RATIO));
42
30
  }
43
31
 
44
- export function queuePersonaTopicMatch(
45
- candidate: PersonaTopicScanCandidate,
32
+ export function queuePersonaTopicRating(
46
33
  context: PersonaTopicContext,
47
- state: StateManager
34
+ state: StateManager,
35
+ options?: PersonaTopicOptions
48
36
  ): void {
49
- const persona = state.persona_getById(context.personaId);
50
- if (!persona) {
51
- console.error(`[queuePersonaTopicMatch] Persona not found: ${context.personaId}`);
52
- return;
53
- }
54
-
55
- const prompt = buildPersonaTopicMatchPrompt({
56
- persona_name: context.personaDisplayName,
57
- candidate,
58
- existing_topics: persona.topics,
59
- });
60
-
61
- state.queue_enqueue({
62
- type: LLMRequestType.JSON,
63
- priority: LLMPriority.Low,
64
- system: prompt.system,
65
- user: prompt.user,
66
- next_step: LLMNextStep.HandlePersonaTopicMatch,
67
- data: {
37
+ const maxTokens = getExtractionMaxTokens(state);
38
+ const { chunks } = chunkExtractionContext(
39
+ {
68
40
  personaId: context.personaId,
69
41
  personaDisplayName: context.personaDisplayName,
70
- candidate,
71
- analyze_from_timestamp: getAnalyzeFromTimestamp(context),
42
+ messages_context: context.messages_context,
43
+ messages_analyze: context.messages_analyze,
72
44
  },
73
- });
74
- }
45
+ maxTokens
46
+ );
75
47
 
76
- export function queuePersonaTopicUpdate(
77
- candidate: PersonaTopicScanCandidate,
78
- matchResult: PersonaTopicMatchResult,
79
- context: PersonaTopicContext,
80
- state: StateManager
81
- ): void {
82
- const persona = state.persona_getById(context.personaId);
83
- if (!persona) {
84
- console.error(`[queuePersonaTopicUpdate] Persona not found: ${context.personaId}`);
48
+ if (chunks.length === 0) {
49
+ console.log(`[queuePersonaTopicRating] No chunks to process for ${context.personaDisplayName}`);
85
50
  return;
86
51
  }
87
52
 
88
- const existingTopic = matchResult.matched_id
89
- ? persona.topics.find((t: PersonaTopic) => t.id === matchResult.matched_id)
90
- : undefined;
53
+ // Mark messages BEFORE queueing to prevent duplicate queueing
54
+ const shortId = context.personaId.slice(0, 8);
55
+ const allAnalyzeIds = context.messages_analyze.map(m => m.id);
56
+ if (options?.roomId) {
57
+ state.markRoomMessagesPersonaExtracted(options.roomId, allAnalyzeIds, shortId);
58
+ } else {
59
+ state.messages_markPersonaExtracted(context.personaId, allAnalyzeIds, shortId);
60
+ }
91
61
 
92
- const prompt = buildPersonaTopicUpdatePrompt({
93
- persona_name: context.personaDisplayName,
94
- short_description: persona.short_description,
95
- long_description: persona.long_description,
96
- traits: persona.traits,
97
- existing_topic: existingTopic,
98
- candidate,
99
- messages_context: context.messages_context,
100
- messages_analyze: context.messages_analyze,
101
- });
62
+ for (const chunk of chunks) {
63
+ const topicsForPrompt = context.topics.map(t => ({
64
+ id: t.id,
65
+ name: t.name,
66
+ description_hint: t.perspective?.slice(0, 80) || t.name,
67
+ }));
102
68
 
103
- state.queue_enqueue({
104
- type: LLMRequestType.JSON,
105
- priority: LLMPriority.Low,
106
- system: prompt.system,
107
- user: prompt.user,
108
- next_step: LLMNextStep.HandlePersonaTopicUpdate,
109
- data: {
110
- personaId: context.personaId,
111
- personaDisplayName: context.personaDisplayName,
112
- candidate,
113
- existingTopicId: existingTopic?.id ?? null,
114
- isNewTopic: !existingTopic,
115
- analyze_from_timestamp: getAnalyzeFromTimestamp(context),
116
- },
117
- });
69
+ const prompt = buildPersonaTopicRatingPrompt({
70
+ persona_name: context.personaDisplayName,
71
+ topics: topicsForPrompt,
72
+ messages_context: chunk.messages_context,
73
+ messages_analyze: chunk.messages_analyze,
74
+ });
75
+
76
+ state.queue_enqueue({
77
+ type: LLMRequestType.JSON,
78
+ priority: LLMPriority.Low,
79
+ system: prompt.system,
80
+ user: prompt.user,
81
+ next_step: LLMNextStep.HandlePersonaTopicRating,
82
+ data: {
83
+ personaId: context.personaId,
84
+ personaDisplayName: context.personaDisplayName,
85
+ message_ids: chunk.messages_analyze.map(m => m.id),
86
+ ceremony_progress: options?.ceremony_progress,
87
+ roomId: options?.roomId,
88
+ },
89
+ });
90
+ }
91
+
92
+ console.log(`[queuePersonaTopicRating] Queued ${chunks.length} rating chunk(s) for ${context.personaDisplayName}`);
118
93
  }
@@ -17,6 +17,7 @@ import {
17
17
  queueEventSummary,
18
18
  type ExtractionContext as HumanExtractionContext,
19
19
  } from "./human-extraction.js";
20
+ import { queuePersonaTopicRating, type PersonaTopicContext } from "./persona-topics.js";
20
21
 
21
22
  const EXTRACTION_BUDGET_RATIO = 0.75;
22
23
  const MIN_EXTRACTION_TOKENS = 10000;
@@ -269,6 +270,24 @@ export function queueRoomCapture(state: StateManager, roomId: string): void {
269
270
  }
270
271
  queueRoomEventScan(roomId, roomDisplayName, allVisible, state, participantContext);
271
272
 
273
+ for (const personaId of room.persona_ids) {
274
+ const shortId = personaId.slice(0, 8);
275
+ const unprocessedRaw = state.getRoomUnextractedMessagesForPersona(roomId, shortId);
276
+ if (unprocessedRaw.length === 0) continue;
277
+ const personaForRoom = state.persona_getById(personaId);
278
+ if (!personaForRoom) continue;
279
+ const processedIds = new Set(allVisible.filter(m => !!m.persona_extracted?.[shortId]).map(m => m.id));
280
+ const personaTopicContext: PersonaTopicContext = {
281
+ personaId,
282
+ personaDisplayName: personaForRoom.display_name,
283
+ messages_context: allVisible.filter(m => processedIds.has(m.id)),
284
+ messages_analyze: normalizeRoomMessages(unprocessedRaw, state),
285
+ topics: personaForRoom.topics,
286
+ };
287
+ queuePersonaTopicRating(personaTopicContext, state, { roomId: roomId });
288
+ console.log(`[queueRoomCapture] Queued persona topic scan: ${personaForRoom.display_name} (${unprocessedRaw.length} messages)`);
289
+ }
290
+
272
291
  console.log(`[queueRoomCapture] Queued extraction for room ${roomDisplayName}`);
273
292
  }
274
293
 
@@ -314,5 +333,20 @@ export function queuePersonaCapture(state: StateManager, personaId: string): voi
314
333
 
315
334
  queueEventSummary(personaId, state, options);
316
335
 
336
+ const shortId = personaId.slice(0, 8);
337
+ const unprocessedForPersona = state.messages_getUnextractedForPersona(personaId, shortId);
338
+ if (unprocessedForPersona.length > 0) {
339
+ const processedIds = new Set(allMessages.filter(m => !!m.persona_extracted?.[shortId]).map(m => m.id));
340
+ const personaTopicContext: PersonaTopicContext = {
341
+ personaId,
342
+ personaDisplayName: persona.display_name,
343
+ messages_context: allMessages.filter(m => processedIds.has(m.id)),
344
+ messages_analyze: unprocessedForPersona,
345
+ topics: persona.topics,
346
+ };
347
+ queuePersonaTopicRating(personaTopicContext, state);
348
+ console.log(`[queuePersonaCapture] Queued persona topic scan for ${persona.display_name} (${unprocessedForPersona.length} messages)`);
349
+ }
350
+
317
351
  console.log(`[queuePersonaCapture] Queued extraction for persona ${persona.display_name}`);
318
352
  }
@@ -7,6 +7,7 @@ import {
7
7
  } from "./types.js";
8
8
  import { StateManager } from "./state-manager.js";
9
9
  import { orchestratePersonaGeneration } from "./orchestrators/index.js";
10
+ import { computePersonaDescriptionEmbedding } from "./embedding-service.js";
10
11
 
11
12
  export async function getPersonaList(sm: StateManager): Promise<PersonaSummary[]> {
12
13
  return sm.persona_getAll().map((entity) => ({
@@ -41,6 +42,9 @@ export async function createPersona(
41
42
  `Cannot create persona with reserved name "${input.name}". Reserved names: ${RESERVED_PERSONA_NAMES.join(", ")}`
42
43
  );
43
44
  }
45
+ if (!input.long_description?.trim()) {
46
+ throw new Error(`Persona "${input.name}" requires a long description.`);
47
+ }
44
48
  const now = new Date().toISOString();
45
49
  const DEFAULT_GROUP = "General";
46
50
  const personaId = crypto.randomUUID();
@@ -117,16 +121,31 @@ export async function updatePersona(
117
121
  ): Promise<boolean> {
118
122
  const persona = sm.persona_getById(personaId);
119
123
  if (!persona) return false;
124
+
125
+ if ('long_description' in updates) {
126
+ const merged = { ...persona, ...updates };
127
+ const embedding = await computePersonaDescriptionEmbedding(merged);
128
+ if (embedding) {
129
+ updates = { ...updates, description_embedding: embedding };
130
+ }
131
+ }
132
+
120
133
  sm.persona_update(personaId, updates);
121
134
  return true;
122
135
  }
123
136
 
124
137
  export async function getGroupList(sm: StateManager): Promise<string[]> {
125
- const personas = sm.persona_getAll();
126
138
  const groups = new Set<string>();
127
- for (const p of personas) {
139
+
140
+ for (const p of sm.persona_getAll()) {
128
141
  if (p.group_primary) groups.add(p.group_primary);
129
142
  for (const g of p.groups_visible || []) groups.add(g);
130
143
  }
144
+
145
+ const human = sm.getHuman();
146
+ for (const item of [...(human.topics || []), ...(human.people || []), ...(human.facts || [])]) {
147
+ for (const g of item.persona_groups || []) groups.add(g);
148
+ }
149
+
131
150
  return [...groups].sort();
132
151
  }
@@ -56,6 +56,7 @@ export async function ensureAgentPersona(
56
56
  sentiment: t.sentiment,
57
57
  strength: t.strength,
58
58
  last_updated: now,
59
+ learned_on: now,
59
60
  }));
60
61
  const persona: PersonaEntity = {
61
62
  id: personaId,