ei-tui 0.4.3 → 0.5.0

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 (101) hide show
  1. package/README.md +14 -0
  2. package/package.json +1 -1
  3. package/src/cli/README.md +17 -12
  4. package/src/cli/commands/personas.ts +12 -0
  5. package/src/cli/mcp.ts +2 -2
  6. package/src/cli/retrieval.ts +86 -8
  7. package/src/cli.ts +8 -5
  8. package/src/core/constants/seed-traits.ts +29 -0
  9. package/src/core/context-utils.ts +1 -0
  10. package/src/core/handlers/human-matching.ts +53 -35
  11. package/src/core/handlers/index.ts +5 -0
  12. package/src/core/handlers/persona-preview.ts +7 -0
  13. package/src/core/handlers/persona-topics.ts +3 -2
  14. package/src/core/handlers/rooms.ts +176 -0
  15. package/src/core/handlers/utils.ts +55 -3
  16. package/src/core/heartbeat-manager.ts +3 -1
  17. package/src/core/llm-client.ts +1 -1
  18. package/src/core/message-manager.ts +10 -8
  19. package/src/core/orchestrators/human-extraction.ts +15 -2
  20. package/src/core/orchestrators/index.ts +1 -0
  21. package/src/core/orchestrators/persona-generation.ts +4 -0
  22. package/src/core/orchestrators/persona-topics.ts +2 -1
  23. package/src/core/orchestrators/room-extraction.ts +318 -0
  24. package/src/core/persona-manager.ts +16 -5
  25. package/src/core/personas/opencode-agent.ts +12 -2
  26. package/src/core/processor.ts +520 -4
  27. package/src/core/prompt-context-builder.ts +89 -5
  28. package/src/core/queue-processor.ts +68 -8
  29. package/src/core/room-manager.ts +408 -0
  30. package/src/core/state/index.ts +1 -0
  31. package/src/core/state/personas.ts +12 -2
  32. package/src/core/state/queue.ts +2 -2
  33. package/src/core/state/rooms.ts +182 -0
  34. package/src/core/state-manager.ts +124 -2
  35. package/src/core/tool-manager.ts +1 -1
  36. package/src/core/tools/index.ts +15 -0
  37. package/src/core/types/enums.ts +11 -0
  38. package/src/core/types/integrations.ts +10 -2
  39. package/src/core/types/llm.ts +3 -0
  40. package/src/core/types/rooms.ts +59 -0
  41. package/src/core/types.ts +1 -0
  42. package/src/core/utils/decay.ts +14 -8
  43. package/src/core/utils/exposure.ts +14 -0
  44. package/src/integrations/claude-code/importer.ts +23 -10
  45. package/src/integrations/cursor/importer.ts +22 -10
  46. package/src/integrations/opencode/importer.ts +30 -13
  47. package/src/prompts/ceremony/dedup.ts +2 -2
  48. package/src/prompts/generation/from-person.ts +85 -0
  49. package/src/prompts/generation/index.ts +2 -0
  50. package/src/prompts/generation/persona.ts +14 -10
  51. package/src/prompts/generation/seeds.ts +4 -29
  52. package/src/prompts/generation/types.ts +13 -0
  53. package/src/prompts/heartbeat/check.ts +1 -1
  54. package/src/prompts/heartbeat/ei.ts +4 -4
  55. package/src/prompts/heartbeat/types.ts +1 -0
  56. package/src/prompts/index.ts +15 -0
  57. package/src/prompts/message-utils.ts +2 -2
  58. package/src/prompts/persona/topics-match.ts +7 -6
  59. package/src/prompts/persona/topics-update.ts +8 -11
  60. package/src/prompts/persona/types.ts +2 -1
  61. package/src/prompts/response/index.ts +1 -1
  62. package/src/prompts/response/sections.ts +20 -8
  63. package/src/prompts/response/types.ts +6 -0
  64. package/src/prompts/room/index.ts +115 -0
  65. package/src/prompts/room/sections.ts +150 -0
  66. package/src/prompts/room/types.ts +93 -0
  67. package/tui/README.md +20 -0
  68. package/tui/src/app.tsx +3 -2
  69. package/tui/src/commands/activate.tsx +98 -0
  70. package/tui/src/commands/archive.tsx +54 -25
  71. package/tui/src/commands/capture.tsx +50 -0
  72. package/tui/src/commands/dedupe.tsx +2 -7
  73. package/tui/src/commands/delete.tsx +48 -0
  74. package/tui/src/commands/details.tsx +7 -0
  75. package/tui/src/commands/persona.tsx +271 -9
  76. package/tui/src/commands/room.tsx +261 -0
  77. package/tui/src/commands/silence.tsx +29 -0
  78. package/tui/src/components/ArchivedItemsOverlay.tsx +144 -0
  79. package/tui/src/components/ConfirmOverlay.tsx +6 -0
  80. package/tui/src/components/ConflictOverlay.tsx +6 -0
  81. package/tui/src/components/HelpOverlay.tsx +6 -1
  82. package/tui/src/components/LoadingOverlay.tsx +51 -0
  83. package/tui/src/components/MessageList.tsx +1 -18
  84. package/tui/src/components/PersonPickerOverlay.tsx +121 -0
  85. package/tui/src/components/PersonaListOverlay.tsx +6 -1
  86. package/tui/src/components/PromptInput.tsx +141 -8
  87. package/tui/src/components/ProviderListOverlay.tsx +5 -1
  88. package/tui/src/components/QuotesOverlay.tsx +5 -1
  89. package/tui/src/components/RoomMessageList.tsx +179 -0
  90. package/tui/src/components/Sidebar.tsx +54 -2
  91. package/tui/src/components/StatusBar.tsx +99 -8
  92. package/tui/src/components/ToolkitListOverlay.tsx +5 -1
  93. package/tui/src/components/WelcomeOverlay.tsx +6 -0
  94. package/tui/src/context/ei.tsx +252 -1
  95. package/tui/src/context/keyboard.tsx +48 -12
  96. package/tui/src/util/cyp-editor.tsx +152 -0
  97. package/tui/src/util/quote-utils.ts +19 -0
  98. package/tui/src/util/room-editor.tsx +164 -0
  99. package/tui/src/util/room-logic.ts +8 -0
  100. package/tui/src/util/room-parser.ts +70 -0
  101. package/tui/src/util/yaml-serializers.ts +151 -0
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Room Response Handlers
3
+ */
4
+
5
+ import { ContextStatus, LLMNextStep, LLMPriority, LLMRequestType } from "../types.js";
6
+ import type { LLMResponse, RoomMessage } from "../types.js";
7
+ import type { StateManager } from "../state-manager.js";
8
+ import type { PersonaResponseResult } from "../../prompts/response/index.js";
9
+ import type { RoomJudgeResult } from "../../prompts/room/index.js";
10
+ import { buildRoomResponsePromptData } from "../prompt-context-builder.js";
11
+
12
+ export function handleRoomResponse(response: LLMResponse, state: StateManager): void {
13
+ const roomId = response.request.data.roomId as string;
14
+ const personaId = response.request.data.personaId as string;
15
+ const personaDisplayName = response.request.data.personaDisplayName as string;
16
+ const parentMessageId = response.request.data.parentMessageId as string | null ?? null;
17
+
18
+ if (!roomId || !personaId) {
19
+ console.error("[handleRoomResponse] Missing roomId or personaId in request data");
20
+ return;
21
+ }
22
+
23
+ const now = new Date().toISOString();
24
+
25
+ if (response.parsed !== undefined) {
26
+ const result = response.parsed as PersonaResponseResult;
27
+
28
+ if (!result.should_respond) {
29
+ const reason = result.reason;
30
+ console.log(`[handleRoomResponse] ${personaDisplayName} chose silence in room ${roomId}: ${reason ?? "(no reason)"}`);
31
+ if (reason) {
32
+ const msg: RoomMessage = {
33
+ id: crypto.randomUUID(),
34
+ parent_id: parentMessageId,
35
+ role: "persona",
36
+ persona_id: personaId,
37
+ silence_reason: reason,
38
+ timestamp: now,
39
+ read: false,
40
+ context_status: ContextStatus.Default,
41
+ };
42
+ state.appendRoomMessage(roomId, msg);
43
+ }
44
+ return;
45
+ }
46
+
47
+ const verbal = result.verbal_response || undefined;
48
+ const action = result.action_response || undefined;
49
+
50
+ if (!verbal && !action) {
51
+ console.log(`[handleRoomResponse] ${personaDisplayName} returned should_respond=true but no content`);
52
+ return;
53
+ }
54
+
55
+ const msg: RoomMessage = {
56
+ id: crypto.randomUUID(),
57
+ parent_id: parentMessageId,
58
+ role: "persona",
59
+ persona_id: personaId,
60
+ verbal_response: verbal,
61
+ action_response: action,
62
+ timestamp: now,
63
+ read: false,
64
+ context_status: ContextStatus.Default,
65
+ };
66
+ state.appendRoomMessage(roomId, msg);
67
+ console.log(`[handleRoomResponse] Appended response from ${personaDisplayName} to room ${roomId}`);
68
+ return;
69
+ }
70
+
71
+ if (!response.content) {
72
+ console.log(`[handleRoomResponse] ${personaDisplayName} no response (empty content)`);
73
+ return;
74
+ }
75
+
76
+ const msg: RoomMessage = {
77
+ id: crypto.randomUUID(),
78
+ parent_id: parentMessageId,
79
+ role: "persona",
80
+ persona_id: personaId,
81
+ verbal_response: response.content,
82
+ timestamp: now,
83
+ read: false,
84
+ context_status: ContextStatus.Default,
85
+ };
86
+ state.appendRoomMessage(roomId, msg);
87
+ console.log(`[handleRoomResponse] Appended plain-text response from ${personaDisplayName} to room ${roomId}`);
88
+ }
89
+
90
+ export async function handleRoomJudge(response: LLMResponse, state: StateManager): Promise<void> {
91
+ const roomId = response.request.data.roomId as string;
92
+ const judgeDisplayName = response.request.data.judgePersonaDisplayName as string;
93
+
94
+ if (!roomId) {
95
+ console.error("[handleRoomJudge] Missing roomId in request data");
96
+ return;
97
+ }
98
+
99
+ if (!response.parsed) {
100
+ console.error(`[handleRoomJudge] No parsed result from judge ${judgeDisplayName}`);
101
+ return;
102
+ }
103
+
104
+ const result = response.parsed as RoomJudgeResult;
105
+ if (!result.winner_message_id) {
106
+ console.error(`[handleRoomJudge] Judge ${judgeDisplayName} returned no winner_message_id`);
107
+ return;
108
+ }
109
+
110
+ const judgePersonaId = response.request.data.judgePersonaId as string;
111
+
112
+ const allMessages = state.getRoomMessages(roomId);
113
+ const winner = allMessages.find(m => m.id === result.winner_message_id);
114
+ if (!winner) {
115
+ console.error(`[handleRoomJudge] Winner message ${result.winner_message_id} not found in room ${roomId}`);
116
+ return;
117
+ }
118
+
119
+ const verdictParentId = winner.parent_id;
120
+
121
+ const ok = state.setRoomActiveNode(roomId, result.winner_message_id);
122
+ if (!ok) {
123
+ console.error(`[handleRoomJudge] Could not set active node ${result.winner_message_id} in room ${roomId}`);
124
+ return;
125
+ }
126
+
127
+ const losers = allMessages
128
+ .filter(m => m.parent_id === verdictParentId && m.id !== winner.id)
129
+ .map(m => m.id);
130
+ if (losers.length > 0) {
131
+ state.removeRoomMessages(roomId, losers);
132
+ }
133
+
134
+ if (result.reason) {
135
+ console.log(`[handleRoomJudge] ${judgeDisplayName} verdict: ${result.reason}`);
136
+ const verdictMsg = {
137
+ id: crypto.randomUUID(),
138
+ parent_id: verdictParentId,
139
+ role: "persona" as const,
140
+ persona_id: judgePersonaId,
141
+ silence_reason: result.reason,
142
+ timestamp: new Date().toISOString(),
143
+ read: false,
144
+ context_status: "default" as import("../types.js").ContextStatus,
145
+ };
146
+ state.appendRoomMessage(roomId, verdictMsg);
147
+ }
148
+
149
+ const room = state.getRoom(roomId);
150
+ if (!room) return;
151
+
152
+ for (const personaId of room.persona_ids) {
153
+ if (room.judge_persona_id === personaId) continue;
154
+ const persona = state.persona_getById(personaId);
155
+ if (!persona || persona.is_archived || persona.is_paused) continue;
156
+
157
+ const isTUI = false;
158
+ const promptData = await buildRoomResponsePromptData(state, room, persona, isTUI);
159
+ const model = persona.model ?? state.getHuman().settings?.default_model ?? "";
160
+
161
+ state.queue_enqueue({
162
+ type: LLMRequestType.JSON,
163
+ priority: LLMPriority.Room,
164
+ system: promptData.system,
165
+ user: promptData.user,
166
+ next_step: LLMNextStep.HandleRoomResponse,
167
+ model,
168
+ data: {
169
+ roomId,
170
+ personaId,
171
+ personaDisplayName: persona.display_name,
172
+ parentMessageId: result.winner_message_id,
173
+ },
174
+ });
175
+ }
176
+ }
@@ -1,12 +1,53 @@
1
- import type { Message, LLMResponse } from "../types.js";
1
+ import type { Message, RoomMessage, LLMResponse } from "../types.js";
2
2
  import type { StateManager } from "../state-manager.js";
3
3
 
4
+ export function normalizeRoomMessages(messages: RoomMessage[], state: StateManager): Message[] {
5
+ const human = state.getHuman();
6
+ const humanName = human.settings?.name_display ?? "Human";
7
+ return messages.map(m => {
8
+ const speakerName = m.role === "human"
9
+ ? humanName
10
+ : (state.persona_getById(m.persona_id ?? "")?.display_name ?? "Participant");
11
+ return {
12
+ id: m.id,
13
+ role: m.role === "human" ? "human" as const : "system" as const,
14
+ speaker_name: speakerName,
15
+ verbal_response: m.verbal_response,
16
+ action_response: m.action_response,
17
+ silence_reason: m.silence_reason,
18
+ timestamp: m.timestamp,
19
+ read: m.read,
20
+ context_status: m.context_status,
21
+ f: m.f,
22
+ t: m.t,
23
+ p: m.p,
24
+ e: m.e,
25
+ };
26
+ });
27
+ }
28
+
4
29
  export function resolveMessageWindow(
5
30
  response: LLMResponse,
6
31
  state: StateManager
7
32
  ): { messages_context: Message[]; messages_analyze: Message[] } {
8
- const personaId = response.request.data.personaId as string;
33
+ const roomId = response.request.data.roomId as string | undefined;
9
34
  const messageIdsToMark = response.request.data.message_ids_to_mark as string[] | undefined;
35
+
36
+ if (roomId) {
37
+ const allRoomMessages = normalizeRoomMessages(state.getRoomMessages(roomId), state);
38
+ if (messageIdsToMark && messageIdsToMark.length > 0) {
39
+ const idSet = new Set(messageIdsToMark);
40
+ const messages_analyze = allRoomMessages.filter(m => idSet.has(m.id));
41
+ const analyzeStartTime = messages_analyze[0]?.timestamp ?? '9999';
42
+ const messages_context = allRoomMessages.filter(m =>
43
+ !idSet.has(m.id) && new Date(m.timestamp).getTime() < new Date(analyzeStartTime).getTime()
44
+ );
45
+ return { messages_context, messages_analyze };
46
+ }
47
+ return { messages_context: [], messages_analyze: allRoomMessages };
48
+ }
49
+
50
+ const personaId = response.request.data.personaId as string;
10
51
  const allMessages = state.messages_get(personaId);
11
52
 
12
53
  if (messageIdsToMark && messageIdsToMark.length > 0) {
@@ -48,10 +89,21 @@ export function markMessagesExtracted(
48
89
  state: StateManager,
49
90
  flag: ExtractionFlag
50
91
  ): void {
92
+ const roomId = response.request.data.roomId as string | undefined;
51
93
  const personaId = response.request.data.personaId as string | undefined;
52
94
  const messageIds = response.request.data.message_ids_to_mark as string[] | undefined;
53
95
 
54
- if (!personaId || !messageIds?.length) return;
96
+ if (!messageIds?.length) return;
97
+
98
+ if (roomId) {
99
+ const count = state.markRoomMessagesExtracted(roomId, messageIds, flag);
100
+ if (count > 0) {
101
+ console.log(`[markMessagesExtracted] Marked ${count} room messages with flag '${flag}' for room ${roomId}`);
102
+ }
103
+ return;
104
+ }
105
+
106
+ if (!personaId) return;
55
107
 
56
108
  const count = state.messages_markExtracted(personaId, messageIds, flag);
57
109
  if (count > 0) {
@@ -158,9 +158,11 @@ export async function queueEiHeartbeat(
158
158
  return;
159
159
  }
160
160
 
161
+ const recentHistory = history.slice(-10);
161
162
  const promptData: EiHeartbeatPromptData = {
162
163
  items,
163
- recent_history: history.slice(-10),
164
+ recent_history: recentHistory,
165
+ system_messages: recentHistory.filter(m => m.role === "system"),
164
166
  };
165
167
 
166
168
  const prompt = buildEiHeartbeatPrompt(promptData);
@@ -159,7 +159,7 @@ export async function callLLMRaw(
159
159
  const chatMessages: ChatMessage[] = [
160
160
  { role: "system", content: systemPrompt },
161
161
  ...messages,
162
- { role: "user", content: userPrompt },
162
+ ...(userPrompt ? [{ role: "user" as const, content: userPrompt }] : []),
163
163
  ];
164
164
 
165
165
  const finalMessages = ensureUserFirst(chatMessages);
@@ -36,7 +36,7 @@ export async function getMessages(
36
36
  ): Promise<Message[]> {
37
37
  const persona = sm.persona_getById(personaId);
38
38
  if (!persona) return [];
39
- return sm.messages_get(personaId);
39
+ return sm.messages_get(personaId).filter(m => m.external !== true);
40
40
  }
41
41
 
42
42
  export async function markMessageRead(
@@ -128,12 +128,13 @@ export async function sendMessage(
128
128
  qp: QueueProcessor,
129
129
  currentRequest: LLMRequest | null,
130
130
  personaId: string,
131
- content: string,
131
+ content: string | null,
132
132
  isTUI: boolean,
133
133
  getModelForPersona: (id?: string) => string | undefined,
134
134
  onError: (err: { code: string; message: string }) => void,
135
135
  onMessageAdded: (id: string) => void,
136
- onMessageQueued: (id: string) => void
136
+ onMessageQueued: (id: string) => void,
137
+ silenceReason?: string
137
138
  ): Promise<void> {
138
139
  const persona = sm.persona_getById(personaId);
139
140
  if (!persona) {
@@ -149,7 +150,8 @@ export async function sendMessage(
149
150
  const message: Message = {
150
151
  id: crypto.randomUUID(),
151
152
  role: "human",
152
- verbal_response: content,
153
+ verbal_response: content ?? undefined,
154
+ silence_reason: content ? undefined : (silenceReason ?? "passed"),
153
155
  timestamp: new Date().toISOString(),
154
156
  read: false,
155
157
  context_status: "default" as ContextStatus,
@@ -158,7 +160,7 @@ export async function sendMessage(
158
160
  onMessageAdded(persona.id);
159
161
 
160
162
  const tools = sm.tools_getForPersona(persona.id, isTUI);
161
- const promptData = await buildResponsePromptData(sm, persona, isTUI, content, tools);
163
+ const promptData = await buildResponsePromptData(sm, persona, isTUI, content ?? "", tools);
162
164
  const prompt = buildResponsePrompt(promptData);
163
165
 
164
166
  sm.queue_enqueue({
@@ -215,7 +217,7 @@ export function checkAndQueueHumanExtraction(
215
217
  ): void {
216
218
  const human = sm.getHuman();
217
219
 
218
- const unextractedFacts = sm.messages_getUnextracted(personaId, "f");
220
+ const unextractedFacts = sm.messages_getUnextracted(personaId, "f", undefined, "exclude");
219
221
  const factsThreshold = Math.min(EXTRACTION_TAPER_CAP, human.facts.filter(f => f.description && f.description !== "").length);
220
222
  if (unextractedFacts.length > 0 && unextractedFacts.length >= factsThreshold) {
221
223
  const context: ExtractionContext = {
@@ -231,7 +233,7 @@ export function checkAndQueueHumanExtraction(
231
233
  );
232
234
  }
233
235
 
234
- const unextractedTopics = sm.messages_getUnextracted(personaId, "t");
236
+ const unextractedTopics = sm.messages_getUnextracted(personaId, "t", undefined, "exclude");
235
237
  const topicsThreshold = Math.min(EXTRACTION_TAPER_CAP, human.topics.length);
236
238
  if (unextractedTopics.length > 0 && unextractedTopics.length >= topicsThreshold) {
237
239
  const context: ExtractionContext = {
@@ -247,7 +249,7 @@ export function checkAndQueueHumanExtraction(
247
249
  );
248
250
  }
249
251
 
250
- const unextractedPeople = sm.messages_getUnextracted(personaId, "p");
252
+ const unextractedPeople = sm.messages_getUnextracted(personaId, "p", undefined, "exclude");
251
253
  const peopleThreshold = Math.min(EXTRACTION_TAPER_CAP, human.people.length);
252
254
  if (unextractedPeople.length > 0 && unextractedPeople.length >= peopleThreshold) {
253
255
  const context: ExtractionContext = {
@@ -58,6 +58,7 @@ export interface ExtractionContext {
58
58
  messages_context: Message[];
59
59
  messages_analyze: Message[];
60
60
  extraction_flag?: "f" | "t" | "p" | "e";
61
+ roomId?: string;
61
62
  }
62
63
 
63
64
  export interface ExtractionOptions {
@@ -67,6 +68,15 @@ export interface ExtractionOptions {
67
68
  extraction_model?: string;
68
69
  /** Override token budget for chunking */
69
70
  extraction_token_limit?: number;
71
+ /**
72
+ * Controls whether external (integration-imported) messages are included.
73
+ * - "exclude": skip messages where external === true
74
+ * - "only": include ONLY messages where external === true
75
+ * - "include": include all messages (backward-compat default; omit means same)
76
+ *
77
+ * NOTE: "include" is the backward-compat default only. All new callers must explicitly pass "exclude" or "only". Will be removed in a future release.
78
+ */
79
+ external_filter?: "include" | "exclude" | "only";
70
80
  }
71
81
 
72
82
  function getAnalyzeFromTimestamp(context: ExtractionContext): string | null {
@@ -466,6 +476,7 @@ export function queueTopicUpdate(
466
476
  if (chunks.length === 0) return 0;
467
477
 
468
478
  for (const chunk of chunks) {
479
+ const primaryPersonaId = context.personaId.split("|")[0];
469
480
  const prompt = buildTopicUpdatePrompt({
470
481
  existing_item: existingItem,
471
482
  new_topic_name: isNewItem ? context.candidateName : undefined,
@@ -474,7 +485,7 @@ export function queueTopicUpdate(
474
485
  messages_context: chunk.messages_context,
475
486
  messages_analyze: chunk.messages_analyze,
476
487
  persona_name: chunk.personaDisplayName,
477
- participant_context: buildParticipantContext(context.personaId, state),
488
+ participant_context: buildParticipantContext(primaryPersonaId, state),
478
489
  });
479
490
 
480
491
  state.queue_enqueue({
@@ -487,6 +498,7 @@ export function queueTopicUpdate(
487
498
  data: {
488
499
  personaId: context.personaId,
489
500
  personaDisplayName: context.personaDisplayName,
501
+ roomId: context.roomId,
490
502
  isNewItem,
491
503
  existingItemId: existingItem?.id,
492
504
  candidateCategory: context.candidateCategory,
@@ -509,7 +521,7 @@ export function queueEventSummary(
509
521
  return 0;
510
522
  }
511
523
 
512
- const unextractedMessages = state.messages_getUnextracted(personaId, "e");
524
+ const unextractedMessages = state.messages_getUnextracted(personaId, "e", undefined, options?.external_filter);
513
525
  if (unextractedMessages.length === 0) {
514
526
  console.log(`[queueEventSummary] No unprocessed messages for ${persona.display_name}`);
515
527
  return 0;
@@ -624,6 +636,7 @@ export function queuePersonUpdate(
624
636
  data: {
625
637
  personaId: context.personaId,
626
638
  personaDisplayName: context.personaDisplayName,
639
+ roomId: context.roomId,
627
640
  isNewItem,
628
641
  existingItemId: existingItem?.id,
629
642
  candidateRelationship: context.candidateRelationship,
@@ -29,3 +29,4 @@ export {
29
29
  queuePersonaTopicUpdate,
30
30
  type PersonaTopicContext,
31
31
  } from "./persona-topics.js";
32
+ export { queueRoomCapture, queuePersonaCapture, checkAndQueueRoomExtraction } from "./room-extraction.js";
@@ -39,12 +39,16 @@ export function orchestratePersonaGeneration(
39
39
  const needsMoreTopics = topicCount < 3;
40
40
 
41
41
  if (needsShortDescription || needsMoreTraits || needsMoreTopics) {
42
+ const filteredTraits = (partial.traits ?? []).filter(t => t.name?.trim());
43
+ const filteredTopics = (partial.topics ?? []).filter(t => t.name?.trim());
42
44
  const prompt = buildPersonaGenerationPrompt({
43
45
  name: partial.name,
44
46
  long_description: partial.long_description,
45
47
  short_description: partial.short_description,
46
48
  existing_traits: partial.traits,
47
49
  existing_topics: partial.topics,
50
+ filtered_traits: filteredTraits,
51
+ filtered_topics: filteredTopics,
48
52
  });
49
53
 
50
54
  stateManager.queue_enqueue({
@@ -110,7 +110,8 @@ export function queuePersonaTopicUpdate(
110
110
  personaId: context.personaId,
111
111
  personaDisplayName: context.personaDisplayName,
112
112
  candidate,
113
- matched_id: matchResult.matched_id,
113
+ existingTopicId: existingTopic?.id ?? null,
114
+ isNewTopic: !existingTopic,
114
115
  analyze_from_timestamp: getAnalyzeFromTimestamp(context),
115
116
  },
116
117
  });