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,5 +1,8 @@
1
1
  import { LLMNextStep } from "../types.js";
2
+ import type { LLMResponse } from "../types.js";
3
+ import type { StateManager } from "../state-manager.js";
2
4
  import type { ResponseHandler } from "./persona-response.js";
5
+ import type { PersonIdentifier } from "../types/data-items.js";
3
6
 
4
7
  export type { ResponseHandler } from "./persona-response.js";
5
8
 
@@ -7,20 +10,60 @@ import { handlePersonaResponse, handleToolContinuation, handleOneShot } from "./
7
10
  import { handleHeartbeatCheck, handleEiHeartbeat } from "./heartbeat.js";
8
11
  import { handlePersonaGeneration, handlePersonaDescriptions, handlePersonaTraitExtraction } from "./persona-generation.js";
9
12
  import {
10
- handlePersonaExpire,
11
- handlePersonaExplore,
12
- handleDescriptionCheck,
13
- handlePersonaTopicScan,
14
- handlePersonaTopicMatch,
15
- handlePersonaTopicUpdate,
13
+ handlePersonaTopicRating,
16
14
  } from "./persona-topics.js";
17
15
  import { handleFactFind, handleHumanTopicScan, handleHumanPersonScan, handleEventScan } from "./human-extraction.js";
18
- import { handleTopicMatch, handleTopicUpdate, handlePersonMatch, handlePersonUpdate } from "./human-matching.js";
16
+ import { handleTopicMatch, handleTopicUpdate, handlePersonUpdate } from "./human-matching.js";
19
17
  import { handleRewriteScan, handleRewriteRewrite } from "./rewrite.js";
20
18
  import { handleDedupCurate } from "./dedup.js";
21
19
  import { handleRoomResponse, handleRoomJudge } from "./rooms.js";
22
20
  import { handlePersonaPreview } from "./persona-preview.js";
23
21
 
22
+ function handlePersonIdentifierMigration(response: LLMResponse, state: StateManager): void {
23
+ const personId = response.request.data.person_id as string;
24
+ if (!personId) {
25
+ console.error("[handlePersonIdentifierMigration] Missing person_id in request data");
26
+ return;
27
+ }
28
+
29
+ const human = state.getHuman();
30
+ const person = human.people.find(p => p.id === personId);
31
+ if (!person) {
32
+ console.error(`[handlePersonIdentifierMigration] Person not found: ${personId}`);
33
+ return;
34
+ }
35
+
36
+ const result = response.parsed as { identifiers?: Array<{ type: string; value: string; is_primary?: boolean }> } | undefined;
37
+ if (!result?.identifiers || !Array.isArray(result.identifiers) || result.identifiers.length === 0) {
38
+ console.error(`[handlePersonIdentifierMigration] Invalid or empty identifiers for ${person.name}`);
39
+ return;
40
+ }
41
+
42
+ const hasName = result.identifiers.some(i => i.value === person.name);
43
+ if (!hasName) {
44
+ result.identifiers.unshift({ type: "nickname", value: person.name });
45
+ }
46
+
47
+ const hasPrimary = result.identifiers.some(i => i.is_primary);
48
+ if (!hasPrimary) {
49
+ result.identifiers[0].is_primary = true;
50
+ }
51
+
52
+ const identifiers: PersonIdentifier[] = result.identifiers.map(i => ({
53
+ type: i.type,
54
+ value: i.value,
55
+ ...(i.is_primary ? { is_primary: i.is_primary } : {}),
56
+ }));
57
+
58
+ state.human_person_upsert({
59
+ ...person,
60
+ identifiers,
61
+ last_updated: new Date().toISOString(),
62
+ });
63
+
64
+ console.log(`[handlePersonIdentifierMigration] Migrated ${identifiers.length} identifier(s) for ${person.name}`);
65
+ }
66
+
24
67
  export const handlers: Record<LLMNextStep, ResponseHandler> = {
25
68
  handlePersonaResponse,
26
69
  handlePersonaGeneration,
@@ -30,18 +73,12 @@ export const handlers: Record<LLMNextStep, ResponseHandler> = {
30
73
  handleHumanPersonScan,
31
74
  handleTopicMatch,
32
75
  handleTopicUpdate,
33
- handlePersonMatch,
34
76
  handlePersonUpdate,
35
77
  handlePersonaTraitExtraction,
36
- handlePersonaTopicScan,
37
- handlePersonaTopicMatch,
38
- handlePersonaTopicUpdate,
78
+ handlePersonaTopicRating,
39
79
  handleHeartbeatCheck,
40
80
  handleEiHeartbeat,
41
81
  handleOneShot,
42
- handlePersonaExpire,
43
- handlePersonaExplore,
44
- handleDescriptionCheck,
45
82
  handleToolContinuation,
46
83
  handleRewriteScan,
47
84
  handleRewriteRewrite,
@@ -50,4 +87,5 @@ export const handlers: Record<LLMNextStep, ResponseHandler> = {
50
87
  handleRoomResponse,
51
88
  handleRoomJudge,
52
89
  handlePersonaPreview,
90
+ [LLMNextStep.HandlePersonIdentifierMigration]: handlePersonIdentifierMigration,
53
91
  };
@@ -36,6 +36,7 @@ export function handlePersonaGeneration(response: LLMResponse, state: StateManag
36
36
  sentiment: userTrait?.sentiment ?? t.sentiment ?? 0,
37
37
  strength: userTrait?.strength ?? t.strength,
38
38
  last_updated: now,
39
+ learned_on: now,
39
40
  };
40
41
  });
41
42
 
@@ -50,6 +51,7 @@ export function handlePersonaGeneration(response: LLMResponse, state: StateManag
50
51
  sentiment: t.sentiment ?? 0,
51
52
  strength: t.strength,
52
53
  last_updated: now,
54
+ learned_on: now,
53
55
  }));
54
56
 
55
57
  const mergedTraits: PersonaTrait[] = mergedLlmTraits.length > 0
@@ -7,6 +7,7 @@ import {
7
7
  import type { StateManager } from "../state-manager.js";
8
8
  import type { PersonaResponseResult } from "../../prompts/response/index.js";
9
9
  import { handlers } from "./index.js";
10
+ import { cleanResponseContent } from "../llm-client.js";
10
11
 
11
12
  export type ResponseHandler = (response: LLMResponse, state: StateManager) => void | Promise<void>;
12
13
 
@@ -18,18 +19,48 @@ export function handlePersonaResponse(response: LLMResponse, state: StateManager
18
19
  return;
19
20
  }
20
21
 
21
- // Always mark user messages as read - even if persona chooses not to respond,
22
- // the messages were "seen" and processed
23
22
  state.messages_markPendingAsRead(personaId);
24
23
 
25
- // Structured JSON path: queue-processor parsed valid JSON into `parsed`
24
+ const raw = cleanResponseContent(response.content ?? "").trim();
25
+
26
+ if (raw.length > 0) {
27
+ const lines = raw.split('\n');
28
+ const isNoResponse = lines[0].replace(/[^a-zA-Z]/g, '').toLowerCase() === 'noresponse';
29
+
30
+ if (isNoResponse) {
31
+ const reason = lines.slice(1).join('\n').trim();
32
+ console.log(`[silence] ${personaDisplayName}: ${reason || "(no reason given)"}`);
33
+ const silentMessage: Message = {
34
+ id: crypto.randomUUID(),
35
+ role: "system",
36
+ silence_reason: reason || undefined,
37
+ timestamp: new Date().toISOString(),
38
+ read: false,
39
+ context_status: ContextStatus.Default,
40
+ };
41
+ state.messages_append(personaId, silentMessage);
42
+ } else {
43
+ const message: Message = {
44
+ id: crypto.randomUUID(),
45
+ role: "system",
46
+ content: raw,
47
+ timestamp: new Date().toISOString(),
48
+ read: false,
49
+ context_status: ContextStatus.Default,
50
+ };
51
+ state.messages_append(personaId, message);
52
+ console.log(`[handlePersonaResponse] Appended Markdown response to ${personaDisplayName}`);
53
+ }
54
+ return;
55
+ }
56
+
26
57
  if (response.parsed !== undefined) {
27
58
  const result = response.parsed as PersonaResponseResult;
28
59
 
29
60
  if (!result.should_respond) {
30
61
  const reason = result.reason;
31
62
  if (reason) {
32
- console.log(`[handlePersonaResponse] ${personaDisplayName} chose silence: ${reason}`);
63
+ console.log(`[silence] ${personaDisplayName}: ${reason}`);
33
64
  const silentMessage: Message = {
34
65
  id: crypto.randomUUID(),
35
66
  role: "system",
@@ -40,12 +71,11 @@ export function handlePersonaResponse(response: LLMResponse, state: StateManager
40
71
  };
41
72
  state.messages_append(personaId, silentMessage);
42
73
  } else {
43
- console.log(`[handlePersonaResponse] ${personaDisplayName} chose not to respond (no reason given)`);
74
+ console.log(`[silence] ${personaDisplayName}: (no reason given)`);
44
75
  }
45
76
  return;
46
77
  }
47
78
 
48
- // Build message with structured fields
49
79
  const verbal = result.verbal_response || undefined;
50
80
  const action = result.action_response || undefined;
51
81
 
@@ -68,22 +98,7 @@ export function handlePersonaResponse(response: LLMResponse, state: StateManager
68
98
  return;
69
99
  }
70
100
 
71
- // Legacy plain-text fallback
72
- if (!response.content) {
73
- console.log(`[handlePersonaResponse] ${personaDisplayName} chose not to respond (no reason given)`);
74
- return;
75
- }
76
-
77
- const message: Message = {
78
- id: crypto.randomUUID(),
79
- role: "system",
80
- verbal_response: response.content ?? undefined,
81
- timestamp: new Date().toISOString(),
82
- read: false,
83
- context_status: ContextStatus.Default,
84
- };
85
- state.messages_append(personaId, message);
86
- console.log(`[handlePersonaResponse] Appended response to ${personaDisplayName}`);
101
+ console.warn(`[silence] ${personaDisplayName}: empty response after cleaning`);
87
102
  }
88
103
 
89
104
  /**
@@ -1,306 +1,70 @@
1
1
  import {
2
- LLMRequestType,
3
- LLMPriority,
4
- LLMNextStep,
5
2
  type LLMResponse,
6
3
  type PersonaTopic,
7
4
  } from "../types.js";
8
5
  import type { StateManager } from "../state-manager.js";
9
- import type { PersonaExpireResult, PersonaExploreResult, DescriptionCheckResult } from "../../prompts/ceremony/types.js";
10
- import type {
11
- PersonaTopicScanResult,
12
- PersonaTopicScanCandidate,
13
- PersonaTopicMatchResult,
14
- PersonaTopicUpdateResult,
15
- } from "../../prompts/persona/types.js";
16
- import {
17
- queueExplorePhase,
18
- queueDescriptionCheck,
19
- queuePersonaTopicMatch,
20
- queuePersonaTopicUpdate,
21
- type PersonaTopicContext,
22
- } from "../orchestrators/index.js";
23
- import { buildPersonaDescriptionsPrompt } from "../../prompts/generation/index.js";
24
- import { splitMessagesByTimestamp } from "./utils.js";
6
+ import type { PersonaTopicRatingResult } from "../../prompts/persona/types.js";
25
7
  import { calculateExposureCurrent } from "../utils/exposure.js";
26
8
 
27
9
  export const MIN_MESSAGE_COUNT_FOR_CREATE = 2;
28
10
 
29
- export function handlePersonaExpire(response: LLMResponse, state: StateManager): void {
11
+ export function handlePersonaTopicRating(response: LLMResponse, state: StateManager): void {
30
12
  const personaId = response.request.data.personaId as string;
31
13
  const personaDisplayName = response.request.data.personaDisplayName as string;
32
- if (!personaId) {
33
- console.error("[handlePersonaExpire] No personaId in request data");
14
+ if (!personaId || !personaDisplayName) {
15
+ console.error("[handlePersonaTopicRating] Missing personaId or personaDisplayName in request data");
34
16
  return;
35
17
  }
36
18
 
37
- const result = response.parsed as PersonaExpireResult | undefined;
38
- const persona = state.persona_getById(personaId);
39
-
40
- if (!persona) {
41
- console.error(`[handlePersonaExpire] Persona not found: ${personaDisplayName}`);
19
+ const result = response.parsed as PersonaTopicRatingResult | undefined;
20
+ if (!result?.ratings || !Array.isArray(result.ratings)) {
21
+ console.log("[handlePersonaTopicRating] No ratings returned or invalid result");
42
22
  return;
43
23
  }
44
24
 
45
- const idsToRemove = new Set(result?.topic_ids_to_remove ?? []);
46
- const remainingTopics = persona.topics.filter((t: PersonaTopic) => !idsToRemove.has(t.id));
47
- const removedCount = persona.topics.length - remainingTopics.length;
48
-
49
- if (removedCount > 0) {
50
- state.persona_update(personaId, {
51
- topics: remainingTopics,
52
- last_updated: new Date().toISOString(),
53
- });
54
- console.log(`[handlePersonaExpire] Removed ${removedCount} topic(s) from ${personaDisplayName}`);
55
- } else {
56
- console.log(`[handlePersonaExpire] No topics removed for ${personaDisplayName}`);
57
- }
58
-
59
- const human = state.getHuman();
60
- const exploreThreshold = human.settings?.ceremony?.explore_threshold ?? 3;
61
-
62
- if (remainingTopics.length < exploreThreshold) {
63
- console.log(`[handlePersonaExpire] ${personaDisplayName} has ${remainingTopics.length} topic(s) (< ${exploreThreshold}), triggering Explore`);
64
- queueExplorePhase(personaId, state);
65
- } else {
66
- queueDescriptionCheck(personaId, state);
67
- }
68
- }
69
-
70
- export function handlePersonaExplore(response: LLMResponse, state: StateManager): void {
71
- const personaId = response.request.data.personaId as string;
72
- const personaDisplayName = response.request.data.personaDisplayName as string;
73
- if (!personaId) {
74
- console.error("[handlePersonaExplore] No personaId in request data");
75
- return;
25
+ const messageIds = response.request.data.message_ids as string[] | undefined;
26
+ if (messageIds?.length) {
27
+ const shortId = personaId.slice(0, 8);
28
+ const roomId = response.request.data.roomId as string | undefined;
29
+ if (roomId) {
30
+ state.markRoomMessagesPersonaExtracted(roomId, messageIds, shortId);
31
+ } else {
32
+ state.messages_markPersonaExtracted(personaId, messageIds, shortId);
33
+ }
76
34
  }
77
35
 
78
- const result = response.parsed as PersonaExploreResult | undefined;
79
36
  const persona = state.persona_getById(personaId);
80
-
81
37
  if (!persona) {
82
- console.error(`[handlePersonaExplore] Persona not found: ${personaDisplayName}`);
83
- queueDescriptionCheck(personaId, state);
84
- return;
85
- }
86
-
87
- const newTopics = result?.new_topics ?? [];
88
- if (newTopics.length === 0) {
89
- console.log(`[handlePersonaExplore] No new topics generated for ${personaDisplayName}`);
90
- queueDescriptionCheck(personaId, state);
38
+ console.error(`[handlePersonaTopicRating] Persona not found: ${personaDisplayName}`);
91
39
  return;
92
40
  }
93
41
 
94
42
  const now = new Date().toISOString();
95
- const existingNames = new Set(persona.topics.map((t: PersonaTopic) => t.name.toLowerCase()));
96
-
97
- const topicsToAdd: PersonaTopic[] = newTopics
98
- .filter(t => !existingNames.has(t.name.toLowerCase()))
99
- .map(t => ({
100
- id: crypto.randomUUID(),
101
- name: t.name,
102
- perspective: t.perspective || "",
103
- approach: t.approach || "",
104
- personal_stake: t.personal_stake || "",
105
- sentiment: t.sentiment,
106
- exposure_current: t.exposure_current ?? 0.2,
107
- exposure_desired: t.exposure_desired ?? 0.6,
108
- last_updated: now,
109
- }));
110
-
111
- if (topicsToAdd.length > 0) {
112
- const allTopics = [...persona.topics, ...topicsToAdd];
113
- state.persona_update(personaId, {
114
- topics: allTopics,
115
- last_updated: now,
116
- });
117
- console.log(`[handlePersonaExplore] Added ${topicsToAdd.length} new topic(s) to ${personaDisplayName}: ${topicsToAdd.map(t => t.name).join(", ")}`);
118
- }
119
-
120
- queueDescriptionCheck(personaId, state);
121
- }
122
-
123
- export function handleDescriptionCheck(response: LLMResponse, state: StateManager): void {
124
- const personaId = response.request.data.personaId as string;
125
- const personaDisplayName = response.request.data.personaDisplayName as string;
126
- if (!personaId) {
127
- console.error("[handleDescriptionCheck] No personaId in request data");
128
- return;
129
- }
130
-
131
- const result = response.parsed as DescriptionCheckResult | undefined;
132
- if (!result) {
133
- console.error("[handleDescriptionCheck] No parsed result");
134
- return;
135
- }
136
-
137
- console.log(`[handleDescriptionCheck] ${personaDisplayName}: ${result.should_update ? "UPDATE NEEDED" : "No update needed"} - ${result.reason ?? "no reason given"}`);
43
+ let updatedCount = 0;
138
44
 
139
- if (!result.should_update) {
140
- console.log(`[handleDescriptionCheck] Ceremony complete for ${personaDisplayName}`);
141
- return;
142
- }
143
-
144
- const persona = state.persona_getById(personaId);
145
- if (!persona) {
146
- console.error(`[handleDescriptionCheck] Persona not found: ${personaDisplayName}`);
147
- return;
148
- }
149
-
150
- const prompt = buildPersonaDescriptionsPrompt({
151
- name: persona.display_name,
152
- aliases: persona.aliases ?? [],
153
- traits: persona.traits,
154
- topics: persona.topics,
155
- });
156
-
157
- state.queue_enqueue({
158
- type: LLMRequestType.JSON,
159
- priority: LLMPriority.Low,
160
- system: prompt.system,
161
- user: prompt.user,
162
- next_step: LLMNextStep.HandlePersonaDescriptions,
163
- data: { personaId, personaDisplayName },
164
- });
165
-
166
- console.log(`[handleDescriptionCheck] Queued description regeneration for ${personaDisplayName}`);
167
- }
168
-
169
- export function handlePersonaTopicScan(response: LLMResponse, state: StateManager): void {
170
- const personaId = response.request.data.personaId as string;
171
- const personaDisplayName = response.request.data.personaDisplayName as string;
172
- if (!personaId || !personaDisplayName) {
173
- console.error("[handlePersonaTopicScan] Missing personaId or personaDisplayName in request data");
174
- return;
175
- }
176
-
177
- const result = response.parsed as PersonaTopicScanResult | undefined;
178
- if (!result?.topics || !Array.isArray(result.topics)) {
179
- console.log("[handlePersonaTopicScan] No topics detected or invalid result");
180
- return;
181
- }
182
-
183
- const analyzeFrom = response.request.data.analyze_from_timestamp as string | null;
184
- const allMessages = state.messages_get(personaId);
185
- const { messages_context, messages_analyze } = splitMessagesByTimestamp(allMessages, analyzeFrom);
186
-
187
- const context: PersonaTopicContext = {
188
- personaId,
189
- personaDisplayName,
190
- messages_context,
191
- messages_analyze,
192
- };
193
-
194
- for (const candidate of result.topics) {
195
- queuePersonaTopicMatch(candidate, context, state);
196
- }
197
- console.log(`[handlePersonaTopicScan] Queued ${result.topics.length} topic(s) for matching`);
198
- }
199
-
200
- export function handlePersonaTopicMatch(response: LLMResponse, state: StateManager): void {
201
- const personaId = response.request.data.personaId as string;
202
- const personaDisplayName = response.request.data.personaDisplayName as string;
203
- const candidate = response.request.data.candidate as PersonaTopicScanCandidate;
204
- const analyzeFrom = response.request.data.analyze_from_timestamp as string | null;
205
-
206
- if (!personaId || !personaDisplayName || !candidate) {
207
- console.error("[handlePersonaTopicMatch] Missing required data");
208
- return;
209
- }
210
-
211
- const result = response.parsed as PersonaTopicMatchResult | undefined;
212
- if (!result) {
213
- console.error("[handlePersonaTopicMatch] No parsed result");
214
- return;
215
- }
216
-
217
- if (result.action === "match") {
218
- console.log(`[handlePersonaTopicMatch] "${candidate.name}" matched existing topic`);
219
- } else if (result.action === "create") {
220
- if (candidate.message_count < MIN_MESSAGE_COUNT_FOR_CREATE) {
221
- console.log(`[handlePersonaTopicMatch] "${candidate.name}" skipped: message_count ${candidate.message_count} < ${MIN_MESSAGE_COUNT_FOR_CREATE}`);
222
- return;
45
+ const updatedTopics = persona.topics.map((topic: PersonaTopic) => {
46
+ const rating = result.ratings.find(r => r.topic_id === topic.id);
47
+ if (!rating || rating.exposure_impact === "none") {
48
+ return topic;
223
49
  }
224
- console.log(`[handlePersonaTopicMatch] "${candidate.name}" will be created`);
225
- } else if (result.action === "skip") {
226
- console.log(`[handlePersonaTopicMatch] "${candidate.name}" skipped: ${result.reason}`);
227
- return;
228
- }
229
50
 
230
- const allMessages = state.messages_get(personaId);
231
- const { messages_context, messages_analyze } = splitMessagesByTimestamp(allMessages, analyzeFrom);
51
+ const newExposure = calculateExposureCurrent(rating.exposure_impact, topic.exposure_current);
52
+ updatedCount++;
232
53
 
233
- const context: PersonaTopicContext = {
234
- personaId,
235
- personaDisplayName,
236
- messages_context,
237
- messages_analyze,
238
- };
239
-
240
- queuePersonaTopicUpdate(candidate, result, context, state);
241
- }
242
-
243
- export function handlePersonaTopicUpdate(response: LLMResponse, state: StateManager): void {
244
- const personaId = response.request.data.personaId as string;
245
- const personaDisplayName = response.request.data.personaDisplayName as string;
246
- const existingTopicId = response.request.data.existingTopicId as string | null;
247
- const isNewTopic = response.request.data.isNewTopic as boolean;
248
-
249
- if (!personaId) {
250
- console.error("[handlePersonaTopicUpdate] No personaId in request data");
251
- return;
252
- }
253
-
254
- const result = response.parsed as PersonaTopicUpdateResult | undefined;
255
- if (!result) {
256
- console.error("[handlePersonaTopicUpdate] No parsed result");
257
- return;
258
- }
259
-
260
- const persona = state.persona_getById(personaId);
261
- if (!persona) {
262
- console.error(`[handlePersonaTopicUpdate] Persona not found: ${personaDisplayName}`);
263
- return;
264
- }
265
-
266
- const now = new Date().toISOString();
267
-
268
- if (isNewTopic) {
269
- const newTopic: PersonaTopic = {
270
- id: crypto.randomUUID(),
271
- name: result.name,
272
- perspective: result.perspective || "",
273
- approach: result.approach || "",
274
- personal_stake: result.personal_stake || "",
275
- sentiment: result.sentiment,
276
- exposure_current: calculateExposureCurrent(result.exposure_impact, 0),
277
- exposure_desired: result.exposure_desired,
54
+ return {
55
+ ...topic,
56
+ exposure_current: newExposure,
278
57
  last_updated: now,
279
58
  };
59
+ });
280
60
 
281
- const allTopics = [...persona.topics, newTopic];
282
- state.persona_update(personaId, { topics: allTopics, last_updated: now });
283
- console.log(`[handlePersonaTopicUpdate] Created new topic "${result.name}" for ${personaDisplayName}`);
284
- } else if (existingTopicId) {
285
- const updatedTopics = persona.topics.map((t: PersonaTopic) => {
286
- if (t.id !== existingTopicId) return t;
287
-
288
- const newExposure = Math.max(calculateExposureCurrent(result.exposure_impact, t.exposure_current), t.exposure_current);
289
-
290
- return {
291
- ...t,
292
- name: result.name,
293
- perspective: result.perspective || t.perspective,
294
- approach: result.approach || t.approach,
295
- personal_stake: result.personal_stake || t.personal_stake,
296
- sentiment: result.sentiment,
297
- exposure_current: newExposure,
298
- exposure_desired: result.exposure_desired,
299
- last_updated: now,
300
- };
61
+ if (updatedCount > 0) {
62
+ state.persona_update(personaId, {
63
+ topics: updatedTopics,
64
+ last_updated: now,
301
65
  });
302
-
303
- state.persona_update(personaId, { topics: updatedTopics, last_updated: now });
304
- console.log(`[handlePersonaTopicUpdate] Updated topic "${result.name}" for ${personaDisplayName}`);
66
+ console.log(`[handlePersonaTopicRating] Updated ${updatedCount}/${persona.topics.length} topics for ${personaDisplayName}`);
67
+ } else {
68
+ console.log(`[handlePersonaTopicRating] No topic exposure updates for ${personaDisplayName}`);
305
69
  }
306
70
  }
@@ -218,6 +218,7 @@ export async function handleRewriteRewrite(response: LLMResponse, state: StateMa
218
218
  description: item.description,
219
219
  sentiment: item.sentiment ?? 0,
220
220
  last_updated: now,
221
+ learned_on: now,
221
222
  learned_by: "ei",
222
223
  persona_groups: unionGroups,
223
224
  interested_personas: unionPersonas,
@@ -244,6 +245,8 @@ export async function handleRewriteRewrite(response: LLMResponse, state: StateMa
244
245
  }
245
246
  const person: Person = {
246
247
  ...baseFields,
248
+ identifiers: [],
249
+ validated_date: '',
247
250
  relationship: item.relationship ?? "Unknown",
248
251
  exposure_current: 0.5,
249
252
  exposure_desired: 0.5,
@@ -8,6 +8,7 @@ import type { StateManager } from "../state-manager.js";
8
8
  import type { PersonaResponseResult } from "../../prompts/response/index.js";
9
9
  import type { RoomJudgeResult } from "../../prompts/room/index.js";
10
10
  import { buildRoomResponsePromptData } from "../prompt-context-builder.js";
11
+ import { cleanResponseContent } from "../llm-client.js";
11
12
 
12
13
  export function handleRoomResponse(response: LLMResponse, state: StateManager): void {
13
14
  const roomId = response.request.data.roomId as string;
@@ -21,13 +22,49 @@ export function handleRoomResponse(response: LLMResponse, state: StateManager):
21
22
  }
22
23
 
23
24
  const now = new Date().toISOString();
25
+ const raw = cleanResponseContent(response.content ?? "").trim();
26
+
27
+ if (raw.length > 0) {
28
+ const lines = raw.split('\n');
29
+ const isNoResponse = lines[0].replace(/[^a-zA-Z]/g, '').toLowerCase() === 'noresponse';
30
+
31
+ if (isNoResponse) {
32
+ const reason = lines.slice(1).join('\n').trim();
33
+ console.log(`[silence] ${personaDisplayName}: ${reason || "(no reason given)"}`);
34
+ const msg: RoomMessage = {
35
+ id: crypto.randomUUID(),
36
+ parent_id: parentMessageId,
37
+ role: "persona",
38
+ persona_id: personaId,
39
+ silence_reason: reason || undefined,
40
+ timestamp: now,
41
+ read: false,
42
+ context_status: ContextStatus.Default,
43
+ };
44
+ state.appendRoomMessage(roomId, msg);
45
+ } else {
46
+ const msg: RoomMessage = {
47
+ id: crypto.randomUUID(),
48
+ parent_id: parentMessageId,
49
+ role: "persona",
50
+ persona_id: personaId,
51
+ content: raw,
52
+ timestamp: now,
53
+ read: false,
54
+ context_status: ContextStatus.Default,
55
+ };
56
+ state.appendRoomMessage(roomId, msg);
57
+ console.log(`[handleRoomResponse] Appended Markdown response from ${personaDisplayName} to room ${roomId}`);
58
+ }
59
+ return;
60
+ }
24
61
 
25
62
  if (response.parsed !== undefined) {
26
63
  const result = response.parsed as PersonaResponseResult;
27
64
 
28
65
  if (!result.should_respond) {
29
66
  const reason = result.reason;
30
- console.log(`[handleRoomResponse] ${personaDisplayName} chose silence in room ${roomId}: ${reason ?? "(no reason)"}`);
67
+ console.log(`[silence] ${personaDisplayName}: ${reason ?? "(no reason given)"}`);
31
68
  if (reason) {
32
69
  const msg: RoomMessage = {
33
70
  id: crypto.randomUUID(),
@@ -64,27 +101,11 @@ export function handleRoomResponse(response: LLMResponse, state: StateManager):
64
101
  context_status: ContextStatus.Default,
65
102
  };
66
103
  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)`);
104
+ console.log(`[handleRoomResponse] Appended structured response from ${personaDisplayName} to room ${roomId}`);
73
105
  return;
74
106
  }
75
107
 
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}`);
108
+ console.warn(`[silence] ${personaDisplayName}: empty response after cleaning`);
88
109
  }
89
110
 
90
111
  export async function handleRoomJudge(response: LLMResponse, state: StateManager): Promise<void> {
@@ -159,7 +180,7 @@ export async function handleRoomJudge(response: LLMResponse, state: StateManager
159
180
  const model = persona.model ?? state.getHuman().settings?.default_model ?? "";
160
181
 
161
182
  state.queue_enqueue({
162
- type: LLMRequestType.JSON,
183
+ type: LLMRequestType.Raw,
163
184
  priority: LLMPriority.Room,
164
185
  system: promptData.system,
165
186
  user: promptData.user,