ei-tui 0.9.2 → 0.9.4

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 (57) hide show
  1. package/package.json +17 -1
  2. package/src/README.md +1 -1
  3. package/src/cli/commands/personas.ts +11 -2
  4. package/src/cli/mcp.ts +2 -2
  5. package/src/cli/retrieval.ts +40 -7
  6. package/src/cli.ts +63 -72
  7. package/src/core/context-utils.ts +2 -2
  8. package/src/core/handlers/heartbeat.ts +9 -1
  9. package/src/core/handlers/human-extraction.ts +4 -1
  10. package/src/core/handlers/human-matching.ts +5 -53
  11. package/src/core/handlers/index.ts +1 -51
  12. package/src/core/handlers/persona-generation.ts +1 -28
  13. package/src/core/handlers/utils.ts +2 -9
  14. package/src/core/heartbeat-manager.ts +4 -4
  15. package/src/core/message-manager.ts +6 -5
  16. package/src/core/orchestrators/ceremony.ts +15 -13
  17. package/src/core/orchestrators/extraction-chunker.ts +3 -3
  18. package/src/core/orchestrators/human-extraction.ts +27 -39
  19. package/src/core/orchestrators/index.ts +0 -1
  20. package/src/core/orchestrators/persona-topics.ts +1 -1
  21. package/src/core/orchestrators/room-extraction.ts +45 -7
  22. package/src/core/processor.ts +8 -21
  23. package/src/core/prompt-context-builder.ts +68 -36
  24. package/src/core/state/personas.ts +1 -17
  25. package/src/core/state-manager.ts +0 -66
  26. package/src/core/types/entities.ts +2 -3
  27. package/src/core/types/enums.ts +0 -2
  28. package/src/core/types/rooms.ts +1 -1
  29. package/src/integrations/claude-code/importer.ts +1 -1
  30. package/src/integrations/cursor/importer.ts +1 -1
  31. package/src/integrations/opencode/importer.ts +1 -1
  32. package/src/prompts/ceremony/index.ts +0 -10
  33. package/src/prompts/ceremony/types.ts +1 -42
  34. package/src/prompts/generation/index.ts +0 -3
  35. package/src/prompts/generation/types.ts +0 -15
  36. package/src/prompts/heartbeat/check.ts +18 -6
  37. package/src/prompts/heartbeat/types.ts +2 -1
  38. package/src/prompts/human/index.ts +0 -2
  39. package/src/prompts/human/person-update.ts +6 -23
  40. package/src/prompts/human/types.ts +0 -16
  41. package/src/prompts/index.ts +0 -19
  42. package/src/prompts/reflection/index.ts +36 -4
  43. package/src/prompts/reflection/types.ts +1 -1
  44. package/src/prompts/response/index.ts +5 -0
  45. package/src/prompts/response/sections.ts +26 -0
  46. package/src/prompts/response/types.ts +3 -0
  47. package/tui/src/commands/registry.test.ts +10 -5
  48. package/tui/src/globals.d.ts +57 -0
  49. package/tui/src/util/yaml-persona.ts +8 -4
  50. package/tui/src/util/yaml-settings.ts +3 -3
  51. package/src/core/orchestrators/person-migration.ts +0 -55
  52. package/src/prompts/ceremony/description-check.ts +0 -54
  53. package/src/prompts/ceremony/expire.ts +0 -37
  54. package/src/prompts/ceremony/explore.ts +0 -77
  55. package/src/prompts/ceremony/person-migration.ts +0 -77
  56. package/src/prompts/generation/descriptions.ts +0 -91
  57. package/src/prompts/human/fact-scan.ts +0 -150
@@ -47,24 +47,48 @@ function capTopicsAndPeople<T extends { id: string }, P extends { id: string }>(
47
47
  // EMBEDDING-BASED RELEVANCE SELECTION
48
48
  // =============================================================================
49
49
 
50
+ const RECENT_MESSAGES_FOR_CONTEXT = 5;
51
+
52
+ async function buildQueryVectors(queries: string[]): Promise<number[][]> {
53
+ const embeddingService = getEmbeddingService();
54
+ return Promise.all(queries.map(q => embeddingService.embed(q)));
55
+ }
56
+
57
+ function unionTopK<T extends { id: string }>(
58
+ candidates: T[],
59
+ queryVectors: number[][],
60
+ limit: number
61
+ ): T[] {
62
+ const best = new Map<string, { item: T; similarity: number }>();
63
+ for (const qv of queryVectors) {
64
+ for (const { item, similarity } of findTopK(qv, candidates, limit)) {
65
+ const existing = best.get(item.id);
66
+ if (!existing || similarity > existing.similarity) {
67
+ best.set(item.id, { item, similarity });
68
+ }
69
+ }
70
+ }
71
+ return Array.from(best.values())
72
+ .filter(({ similarity }) => similarity >= SIMILARITY_THRESHOLD)
73
+ .sort((a, b) => b.similarity - a.similarity)
74
+ .slice(0, limit)
75
+ .map(({ item }) => item);
76
+ }
77
+
50
78
  async function selectRelevantItems<T extends { id: string; embedding?: number[] }>(
51
79
  items: T[],
52
80
  limit: number,
53
- currentMessage?: string
81
+ queries: string[]
54
82
  ): Promise<T[]> {
55
83
  if (items.length === 0) return [];
56
84
 
57
85
  const withEmbeddings = items.filter((i) => i.embedding?.length);
86
+ const activeQueries = queries.filter(Boolean);
58
87
 
59
- if (currentMessage && withEmbeddings.length > 0) {
88
+ if (activeQueries.length > 0 && withEmbeddings.length > 0) {
60
89
  try {
61
- const embeddingService = getEmbeddingService();
62
- const queryVector = await embeddingService.embed(currentMessage);
63
- const results = findTopK(queryVector, withEmbeddings, limit);
64
- const relevant = results
65
- .filter(({ similarity }) => similarity >= SIMILARITY_THRESHOLD)
66
- .map(({ item }) => item);
67
-
90
+ const queryVectors = await buildQueryVectors(activeQueries);
91
+ const relevant = unionTopK(withEmbeddings, queryVectors, limit);
68
92
  if (relevant.length > 0) return relevant;
69
93
  } catch (err) {
70
94
  console.warn("[filterHumanDataByVisibility] Embedding search failed:", err);
@@ -80,19 +104,15 @@ async function selectRelevantItems<T extends { id: string; embedding?: number[]
80
104
  .slice(0, limit);
81
105
  }
82
106
 
83
- async function selectRelevantQuotes(quotes: Quote[], currentMessage?: string): Promise<Quote[]> {
107
+ async function selectRelevantQuotes(quotes: Quote[], queries: string[]): Promise<Quote[]> {
84
108
  if (quotes.length === 0) return [];
85
109
  const withEmbeddings = quotes.filter((q) => q.embedding?.length);
110
+ const activeQueries = queries.filter(Boolean);
86
111
 
87
- if (currentMessage && withEmbeddings.length > 0) {
112
+ if (activeQueries.length > 0 && withEmbeddings.length > 0) {
88
113
  try {
89
- const embeddingService = getEmbeddingService();
90
- const queryVector = await embeddingService.embed(currentMessage);
91
- const results = findTopK(queryVector, withEmbeddings, QUOTE_LIMIT);
92
- const relevant = results
93
- .filter(({ similarity }) => similarity >= SIMILARITY_THRESHOLD)
94
- .map(({ item }) => item);
95
-
114
+ const queryVectors = await buildQueryVectors(activeQueries);
115
+ const relevant = unionTopK(withEmbeddings, queryVectors, QUOTE_LIMIT);
96
116
  if (relevant.length > 0) return relevant;
97
117
  } catch (err) {
98
118
  console.warn("[filterHumanDataByVisibility] Embedding search failed:", err);
@@ -110,16 +130,16 @@ async function selectRelevantQuotes(quotes: Quote[], currentMessage?: string): P
110
130
  export async function filterHumanDataByVisibility(
111
131
  human: HumanEntity,
112
132
  persona: PersonaEntity,
113
- currentMessage?: string
133
+ queries: string[]
114
134
  ): Promise<ResponsePromptData["human"]> {
115
135
  const DEFAULT_GROUP = "General";
116
136
 
117
137
  if (persona.id === "ei") {
118
138
  const [facts, rawTopics, rawPeople, quotes] = await Promise.all([
119
- selectRelevantItems(human.facts, DATA_ITEM_LIMIT, currentMessage),
120
- selectRelevantItems(human.topics, DATA_ITEM_LIMIT, currentMessage),
121
- selectRelevantItems(human.people, DATA_ITEM_LIMIT, currentMessage),
122
- selectRelevantQuotes(human.quotes ?? [], currentMessage),
139
+ selectRelevantItems(human.facts, DATA_ITEM_LIMIT, queries),
140
+ selectRelevantItems(human.topics, DATA_ITEM_LIMIT, queries),
141
+ selectRelevantItems(human.people, DATA_ITEM_LIMIT, queries),
142
+ selectRelevantQuotes(human.quotes ?? [], queries),
123
143
  ]);
124
144
  const { topics, people } = capTopicsAndPeople(rawTopics, rawPeople);
125
145
  const humanName =
@@ -157,10 +177,10 @@ export async function filterHumanDataByVisibility(
157
177
  });
158
178
 
159
179
  const [facts, rawTopics, rawPeople, quotes] = await Promise.all([
160
- selectRelevantItems(filterByGroup(human.facts), DATA_ITEM_LIMIT, currentMessage),
161
- selectRelevantItems(filterByGroup(human.topics), DATA_ITEM_LIMIT, currentMessage),
162
- selectRelevantItems(filterByGroup(human.people), DATA_ITEM_LIMIT, currentMessage),
163
- selectRelevantQuotes(groupFilteredQuotes, currentMessage),
180
+ selectRelevantItems(filterByGroup(human.facts), DATA_ITEM_LIMIT, queries),
181
+ selectRelevantItems(filterByGroup(human.topics), DATA_ITEM_LIMIT, queries),
182
+ selectRelevantItems(filterByGroup(human.people), DATA_ITEM_LIMIT, queries),
183
+ selectRelevantQuotes(groupFilteredQuotes, queries),
164
184
  ]);
165
185
  const { topics, people } = capTopicsAndPeople(rawTopics, rawPeople);
166
186
 
@@ -233,14 +253,25 @@ export async function buildResponsePromptData(
233
253
  tools?: import("./types.js").ToolDefinition[]
234
254
  ): Promise<ResponsePromptData> {
235
255
  const human = sm.getHuman();
236
- const filteredHuman = await filterHumanDataByVisibility(human, persona, currentMessage);
237
- const visiblePersonas = getVisiblePersonas(sm, persona);
238
256
  const messages = sm.messages_get(persona.id);
239
257
  const previousMessage = messages.length >= 2 ? messages[messages.length - 2] : null;
240
258
  const delayMs = previousMessage
241
259
  ? Date.now() - new Date(previousMessage.timestamp).getTime()
242
260
  : 0;
243
261
 
262
+ const recentMessageContents = messages
263
+ .slice(-RECENT_MESSAGES_FOR_CONTEXT - 1, -1)
264
+ .map(m => getMessageContent(m))
265
+ .filter(Boolean);
266
+
267
+ const queries = [
268
+ ...(currentMessage ? [currentMessage] : []),
269
+ ...recentMessageContents,
270
+ ];
271
+
272
+ const filteredHuman = await filterHumanDataByVisibility(human, persona, queries);
273
+ const visiblePersonas = getVisiblePersonas(sm, persona);
274
+
244
275
  const alwaysMessages = sm.messages_getAlways(persona.id);
245
276
  const temporalAnchors = alwaysMessages.map(m => ({
246
277
  role: m.role === "human" ? "human" as const : "system" as const,
@@ -260,6 +291,7 @@ export async function buildResponsePromptData(
260
291
  topics: persona.topics,
261
292
  interested_topics: persona.topics.filter(t => t.exposure_desired - t.exposure_current > 0.2),
262
293
  include_message_timestamps: persona.include_message_timestamps,
294
+ pending_update: persona.pending_update,
263
295
  },
264
296
  human: filteredHuman,
265
297
  visible_personas: visiblePersonas,
@@ -287,10 +319,10 @@ export async function buildRoomResponsePromptData(
287
319
 
288
320
  let sourceMessages: RoomMessage[];
289
321
  if (room.mode === RoomMode.FreeForAll) {
290
- const contextWindowHours = room.context_window_hours
291
- ?? human.settings?.default_context_window_hours
292
- ?? 8;
293
- const windowCutoff = new Date(Date.now() - contextWindowHours * 60 * 60 * 1000).toISOString();
322
+ const contextWindowMs = room.context_window_ms
323
+ ?? human.settings?.default_context_window_ms
324
+ ?? 28800000;
325
+ const windowCutoff = new Date(Date.now() - contextWindowMs).toISOString();
294
326
  const boundaryMs = room.context_boundary ? new Date(room.context_boundary).getTime() : 0;
295
327
  sourceMessages = allSourceMessages.filter(m => {
296
328
  const msgMs = new Date(m.timestamp).getTime();
@@ -303,8 +335,8 @@ export async function buildRoomResponsePromptData(
303
335
  const byCount = allSourceMessages.slice(-MIN_ROOM_MESSAGES);
304
336
  if (byCount.length > sourceMessages.length) sourceMessages = byCount;
305
337
  } else {
306
- const contextWindowHours = human.settings?.default_context_window_hours ?? 8;
307
- const windowCutoff = new Date(Date.now() - contextWindowHours * 60 * 60 * 1000).toISOString();
338
+ const contextWindowMs = human.settings?.default_context_window_ms ?? 28800000;
339
+ const windowCutoff = new Date(Date.now() - contextWindowMs).toISOString();
308
340
  const byTime = allSourceMessages.filter(m => m.timestamp >= windowCutoff);
309
341
  const byCount = allSourceMessages.slice(-MIN_ROOM_MESSAGES);
310
342
  sourceMessages = byTime.length >= byCount.length ? byTime : byCount;
@@ -313,7 +345,7 @@ export async function buildRoomResponsePromptData(
313
345
  const lastMessage = sourceMessages[sourceMessages.length - 1];
314
346
  const currentMessage = lastMessage ? getMessageContent(lastMessage) : undefined;
315
347
 
316
- const filteredHuman = await filterHumanDataByVisibility(human, respondingPersona, currentMessage);
348
+ const filteredHuman = await filterHumanDataByVisibility(human, respondingPersona, currentMessage ? [currentMessage] : []);
317
349
 
318
350
  const history = normalizeRoomMessages(sourceMessages, sm);
319
351
 
@@ -1,21 +1,5 @@
1
1
  import type { PersonaEntity, Message, ContextStatus } from "../types.js";
2
2
 
3
- // TODO(v1.0.0): Remove LegacyMessage migration — verbal_response/action_response no longer written
4
- type LegacyMessage = Message & { verbal_response?: string; action_response?: string };
5
-
6
- function migrateMessage(msg: Message): Message {
7
- if (msg.content) return msg;
8
- if (msg.silence_reason) return msg;
9
- const legacy = msg as LegacyMessage;
10
- const hasLegacy = 'verbal_response' in legacy || 'action_response' in legacy;
11
- if (!hasLegacy) return msg;
12
- const parts: string[] = [];
13
- if (legacy.action_response) parts.push(`_${legacy.action_response}_`);
14
- if (legacy.verbal_response) parts.push(legacy.verbal_response);
15
- const { verbal_response: _vr, action_response: _ar, ...rest } = legacy;
16
- return parts.length > 0 ? { ...rest, content: parts.join('\n\n') } : rest as Message;
17
- }
18
-
19
3
  export interface PersonaData {
20
4
  entity: PersonaEntity;
21
5
  messages: Message[];
@@ -29,7 +13,7 @@ export class PersonaState {
29
13
  this.personas = new Map(
30
14
  Object.entries(personas).map(([id, data]) => [
31
15
  id,
32
- { entity: data.entity, messages: data.messages.map(migrateMessage) },
16
+ { entity: data.entity, messages: data.messages },
33
17
  ])
34
18
  );
35
19
  }
@@ -69,76 +69,10 @@ export class StateManager {
69
69
  this.migrateMessageFlags();
70
70
  this.migrateInterestedPersonas();
71
71
  this.migrateProviderModel();
72
- this.migratePersonaMessageContent();
73
- this.migrateRoomMessageContent();
74
72
  this.migrateThemes();
75
73
  this.migrateFfaParentIds();
76
74
  }
77
75
 
78
- private migratePersonaMessageContent(): void {
79
- // TODO(v1.0.0): Remove legacy persona message migration — verbal_response/action_response no longer written
80
- const rawPersonas = (this.personaState as unknown as { personas: Map<string, { messages: Message[] }> }).personas;
81
- let migratedCount = 0;
82
- for (const [, data] of rawPersonas) {
83
- const messages = data.messages;
84
- for (const msg of messages) {
85
- const legacy = msg as Message & { verbal_response?: string; action_response?: string };
86
- if (!('verbal_response' in legacy || 'action_response' in legacy)) continue;
87
- if (msg.content) {
88
- delete (legacy as any).verbal_response;
89
- delete (legacy as any).action_response;
90
- migratedCount++;
91
- continue;
92
- }
93
- if (msg.silence_reason) {
94
- delete (legacy as any).verbal_response;
95
- delete (legacy as any).action_response;
96
- migratedCount++;
97
- continue;
98
- }
99
- const parts: string[] = [];
100
- if (legacy.action_response) parts.push(`_${legacy.action_response}_`);
101
- if (legacy.verbal_response) parts.push(legacy.verbal_response);
102
- if (parts.length > 0) (msg as any).content = parts.join('\n\n');
103
- delete (legacy as any).verbal_response;
104
- delete (legacy as any).action_response;
105
- migratedCount++;
106
- }
107
- }
108
- if (migratedCount > 0) {
109
- this.scheduleSave();
110
- console.log(`[StateManager] Migrated ${migratedCount} persona messages to unified content field`);
111
- }
112
- }
113
-
114
- private migrateRoomMessageContent(): void {
115
- const rooms = this.roomState.getAll(true);
116
- let migratedCount = 0;
117
-
118
- for (const room of rooms) {
119
- for (const msg of room.messages) {
120
- if (msg.content) continue;
121
- if (msg.silence_reason) continue;
122
- // TODO(v1.0.0): Remove legacy room message migration — verbal_response/action_response no longer written
123
- const legacy = msg as RoomMessage & { verbal_response?: string; action_response?: string };
124
- const hasLegacy = 'verbal_response' in legacy || 'action_response' in legacy;
125
- if (!hasLegacy) continue;
126
- const parts: string[] = [];
127
- if (legacy.action_response) parts.push(`_${legacy.action_response}_`);
128
- if (legacy.verbal_response) parts.push(legacy.verbal_response);
129
- if (parts.length > 0) msg.content = parts.join('\n\n');
130
- delete (msg as any).verbal_response;
131
- delete (msg as any).action_response;
132
- migratedCount++;
133
- }
134
- }
135
-
136
- if (migratedCount > 0) {
137
- this.scheduleSave();
138
- console.log(`[StateManager] Migrated ${migratedCount} room messages to unified content field`);
139
- }
140
- }
141
-
142
76
  /**
143
77
  * Migration: learned_by used to store display names; now stores persona IDs.
144
78
  * On load, attempt to resolve display names -> IDs using current persona map.
@@ -103,12 +103,11 @@ export interface HumanSettings {
103
103
  default_model?: string; // Will store ModelConfig.id GUID post-migration
104
104
  oneshot_model?: string; // Model for AI-assist (wand) requests; falls back to default_model. Will store ModelConfig.id GUID post-migration.
105
105
  rewrite_model?: string; // Model for rewrite ceremony step; must be capable (Sonnet/Opus class). Unset = rewrite disabled. Will store ModelConfig.id GUID post-migration.
106
- people_migration_complete?: boolean; // Set to true when all Person records have identifiers. Ceremony migration step short-circuits when true.
107
106
  queue_paused?: boolean;
108
107
  skip_quote_delete_confirm?: boolean;
109
108
  name_display?: string;
110
109
  default_heartbeat_ms?: number;
111
- default_context_window_hours?: number;
110
+ default_context_window_ms?: number;
112
111
  message_min_count?: number;
113
112
  message_max_age_days?: number;
114
113
  accounts?: ProviderAccount[];
@@ -150,7 +149,7 @@ export interface PersonaEntity {
150
149
  archived_at?: string;
151
150
  is_static: boolean;
152
151
  heartbeat_delay_ms?: number;
153
- context_window_hours?: number;
152
+ context_window_ms?: number;
154
153
  include_message_timestamps?: boolean; // Prepend ISO timestamp to each message sent to the LLM
155
154
  context_boundary?: string; // ISO timestamp - messages before this excluded from LLM context
156
155
  last_updated: string;
@@ -26,7 +26,6 @@ export enum LLMPriority {
26
26
  export enum LLMNextStep {
27
27
  HandlePersonaResponse = "handlePersonaResponse",
28
28
  HandlePersonaGeneration = "handlePersonaGeneration",
29
- HandlePersonaDescriptions = "handlePersonaDescriptions",
30
29
  HandleFactFind = "handleFactFind",
31
30
  HandleHumanTopicScan = "handleHumanTopicScan",
32
31
  HandleHumanPersonScan = "handleHumanPersonScan",
@@ -51,7 +50,6 @@ export enum LLMNextStep {
51
50
  HandleRoomResponse = "handleRoomResponse",
52
51
  HandleRoomJudge = "handleRoomJudge",
53
52
  HandlePersonaPreview = "handlePersonaPreview",
54
- HandlePersonIdentifierMigration = "handlePersonIdentifierMigration",
55
53
  HandleTopicValidate = "handleTopicValidate",
56
54
  HandleReflectionCritic = "handleReflectionCritic",
57
55
  }
@@ -35,7 +35,7 @@ export interface RoomEntity {
35
35
  created_at: string;
36
36
  last_updated: string;
37
37
  capture_used?: boolean;
38
- context_window_hours?: number; // FFA only; falls back to human.settings.default_context_window_hours
38
+ context_window_ms?: number; // FFA only; falls back to human.settings.default_context_window_ms
39
39
  context_boundary?: string; // FFA only; ISO timestamp; same semantics as persona context_boundary
40
40
  messages: RoomMessage[];
41
41
  }
@@ -262,7 +262,7 @@ export async function importClaudeCodeSessions(
262
262
 
263
263
  const context: ExtractionContext = {
264
264
  personaId: persona.id,
265
- personaDisplayName: persona.display_name,
265
+ channelDisplayName: persona.display_name,
266
266
  messages_context: contextMsgs,
267
267
  messages_analyze: toAnalyze,
268
268
  sources: [`claudecode:${getMachineId()}:${targetSession.id}`],
@@ -221,7 +221,7 @@ export async function importCursorSessions(
221
221
 
222
222
  const context: ExtractionContext = {
223
223
  personaId: persona.id,
224
- personaDisplayName: persona.display_name,
224
+ channelDisplayName: persona.display_name,
225
225
  messages_context: contextMsgs,
226
226
  messages_analyze: toAnalyze,
227
227
  sources: [`cursor:${getMachineId()}:${targetSession.id}`],
@@ -245,7 +245,7 @@ export async function importOpenCodeSessions(
245
245
 
246
246
  const context: ExtractionContext = {
247
247
  personaId: persona.id,
248
- personaDisplayName: persona.display_name,
248
+ channelDisplayName: persona.display_name,
249
249
  messages_context: contextMsgs,
250
250
  messages_analyze: toAnalyze,
251
251
  sources: [`opencode:${getMachineId()}:${targetSession.id}`],
@@ -1,17 +1,7 @@
1
- export { buildPersonaExpirePrompt } from "./expire.js";
2
- export { buildPersonaExplorePrompt } from "./explore.js";
3
- export { buildDescriptionCheckPrompt } from "./description-check.js";
4
1
  export { buildRewriteScanPrompt, buildRewritePrompt } from "./rewrite.js";
5
2
  export { buildDedupPrompt, buildValidatePrompt } from "./dedup.js";
6
3
  export { buildUserDedupPrompt } from "./user-dedup.js";
7
- export { buildPersonMigrationPrompt, type PersonMigrationPromptData } from "./person-migration.js";
8
4
  export type {
9
- PersonaExpirePromptData,
10
- PersonaExpireResult,
11
- PersonaExplorePromptData,
12
- PersonaExploreResult,
13
- DescriptionCheckPromptData,
14
- DescriptionCheckResult,
15
5
  RewriteItemType,
16
6
  RewriteScanPromptData,
17
7
  RewriteScanResult,
@@ -1,45 +1,4 @@
1
- import type { PersonaTrait, PersonaTopic, DataItemBase } from "../../core/types.js";
2
-
3
- export interface PersonaExpirePromptData {
4
- persona_name: string;
5
- topics: PersonaTopic[];
6
- }
7
-
8
- export interface PersonaExpireResult {
9
- topic_ids_to_remove: string[];
10
- }
11
-
12
- export interface PersonaExplorePromptData {
13
- persona_name: string;
14
- traits: PersonaTrait[];
15
- remaining_topics: PersonaTopic[];
16
- recent_conversation_themes: string[];
17
- }
18
-
19
- export interface PersonaExploreResult {
20
- new_topics: Array<{
21
- name: string;
22
- perspective: string;
23
- approach: string;
24
- personal_stake: string;
25
- sentiment: number;
26
- exposure_current: number;
27
- exposure_desired: number;
28
- }>;
29
- }
30
-
31
- export interface DescriptionCheckPromptData {
32
- persona_name: string;
33
- current_short_description?: string;
34
- current_long_description?: string;
35
- traits: PersonaTrait[];
36
- topics: PersonaTopic[];
37
- }
38
-
39
- export interface DescriptionCheckResult {
40
- should_update: boolean;
41
- reason?: string;
42
- }
1
+ import type { DataItemBase } from "../../core/types.js";
43
2
 
44
3
  // =============================================================================
45
4
  // REWRITE (Item Reorganization)
@@ -1,5 +1,4 @@
1
1
  export { buildPersonaGenerationPrompt } from "./persona.js";
2
- export { buildPersonaDescriptionsPrompt } from "./descriptions.js";
3
2
  export { buildPersonaFromPersonPrompt } from "./from-person.js";
4
3
  export {
5
4
  DEFAULT_SEED_TRAITS,
@@ -11,7 +10,5 @@ export type {
11
10
  PersonaGenerationPromptData,
12
11
  PersonaGenerationResult,
13
12
  PersonaFromPersonPromptData,
14
- PersonaDescriptionsPromptData,
15
- PersonaDescriptionsResult,
16
13
  PromptOutput,
17
14
  } from "./types.js";
@@ -1,5 +1,3 @@
1
- import type { PersonaTrait, PersonaTopic } from "../../core/types.js";
2
-
3
1
  export interface PromptOutput {
4
2
  system: string;
5
3
  user: string;
@@ -45,16 +43,3 @@ export interface PersonaFromPersonPromptData {
45
43
  existing_trait_names?: string[];
46
44
  existing_topic_names?: string[];
47
45
  }
48
-
49
- export interface PersonaDescriptionsPromptData {
50
- name: string;
51
- aliases: string[];
52
- traits: PersonaTrait[];
53
- topics: PersonaTopic[];
54
- }
55
-
56
- export interface PersonaDescriptionsResult {
57
- short_description: string;
58
- long_description: string;
59
- no_change?: boolean;
60
- }
@@ -74,7 +74,6 @@ function getLastPersonaMessage(history: Message[]): Message | undefined {
74
74
  * - Getting recent message history
75
75
  */
76
76
  export function buildHeartbeatCheckPrompt(data: HeartbeatCheckPromptData): PromptOutput {
77
- console.log(`[HeartbeatCheck ${data.persona.name}] Building prompt - topics: ${data.human.topics.length}, people: ${data.human.people.length}, inactive_days: ${data.inactive_days}, history: ${data.recent_history.length} messages`);
78
77
  if (!data.persona?.name) {
79
78
  throw new Error("buildHeartbeatCheckPrompt: persona.name is required");
80
79
  }
@@ -124,11 +123,24 @@ ${formatPeopleWithGaps(data.human.people)}`;
124
123
 
125
124
  **Quality over quantity** - Only reach out if you have something real to say.`;
126
125
 
127
- const pendingUpdateFragment = data.persona.has_pending_update
128
- ? `## Pending Identity Changes
129
-
130
- Based on recent conversations, there are proposed updates to your identity waiting for review. The user has been notified and can review them. You may bring this up if it feels right — or not. It's yours to decide.`
131
- : '';
126
+ const pendingUpdateFragment = data.persona.pending_update ? (() => {
127
+ const p = data.persona.pending_update!;
128
+ const descPart = p.long_description || p.short_description
129
+ ? `### Proposed Description\n${p.long_description || p.short_description}`
130
+ : "";
131
+ const traitsPart = p.traits.length > 0
132
+ ? `### Proposed Traits\n${p.traits.map(t => `- **${t.name}**: ${t.description}`).join("\n")}`
133
+ : "";
134
+ const topicsPart = p.topics.length > 0
135
+ ? `### Proposed Interests\n${p.topics.map(t => `- **${t.name}**: ${t.perspective}`).join("\n")}`
136
+ : "";
137
+ const parts = [descPart, traitsPart, topicsPart].filter(Boolean).join("\n\n");
138
+ return `## Pending Identity Changes
139
+
140
+ Your human is reviewing proposed updates to your identity. These are waiting for their response — you may want to bring it up, or not. It's yours to decide.
141
+
142
+ ${parts}`;
143
+ })() : '';
132
144
 
133
145
  const outputFragment = `## Response Format
134
146
 
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import type { PersonaTrait, Topic, Person, Message, PersonaTopic } from "../../core/types.js";
7
+ import type { PersonaEntity } from "../../core/types/entities.js";
7
8
 
8
9
  /**
9
10
  * Common prompt output structure
@@ -21,7 +22,7 @@ export interface HeartbeatCheckPromptData {
21
22
  name: string;
22
23
  traits: PersonaTrait[];
23
24
  topics: PersonaTopic[];
24
- has_pending_update: boolean;
25
+ pending_update?: PersonaEntity["pending_update"];
25
26
  };
26
27
  human: {
27
28
  topics: Topic[]; // Filtered, sorted by engagement gap
@@ -1,4 +1,3 @@
1
- export { buildHumanFactScanPrompt } from "./fact-scan.js";
2
1
  export { buildFactFindPrompt } from "./fact-find.js";
3
2
  export { buildHumanTopicScanPrompt } from "./topic-scan.js";
4
3
  export { buildHumanPersonScanPrompt } from "./person-scan.js";
@@ -16,7 +15,6 @@ export type {
16
15
  PromptOutput,
17
16
  ParticipantContext,
18
17
  PersonaEntitySnapshot,
19
- FactScanPromptData,
20
18
  TopicScanPromptData,
21
19
  PersonScanPromptData,
22
20
  FactFindPromptData,
@@ -1,4 +1,4 @@
1
- import type { PromptOutput, ParticipantContext, PersonaEntitySnapshot } from "./types.js";
1
+ import type { PromptOutput, ParticipantContext } from "./types.js";
2
2
  import type { Person, Message } from "../../core/types.js";
3
3
  import { formatMessagesAsPlaceholders } from "../message-utils.js";
4
4
 
@@ -12,7 +12,6 @@ export interface PersonUpdatePromptData {
12
12
  persona_name: string;
13
13
  participant_context?: ParticipantContext;
14
14
  known_identifier_types?: string[];
15
- persona_entity?: PersonaEntitySnapshot;
16
15
  }
17
16
 
18
17
  function participantContextSection(ctx: ParticipantContext | undefined): string {
@@ -121,38 +120,22 @@ Detail you add should:
121
120
  **ABSOLUTELY VITAL**: Do **NOT** embellish. Record only what the user actually said or demonstrated.`;
122
121
 
123
122
  } else if (isEiPersona) {
124
- const entityRef = data.persona_entity
125
- ? `## This Persona's Defined Identity (for reference)
126
-
127
- The following is ${personName}'s current self-definition — their traits, topics, and long description as set in the Persona editor. Use this as a **baseline**, not a ceiling.
128
-
129
- **Long description:**
130
- ${data.persona_entity.long_description}
131
-
132
- **Traits:**
133
- ${data.persona_entity.traits.map(t => `- **${t.name}**: ${t.description}`).join('\n')}
134
-
135
- **Topics:**
136
- ${data.persona_entity.topics.map(t => `- **${t.name}**: ${t.perspective}`).join('\n')}
137
-
138
- `
139
- : '';
140
-
141
- descriptionSection = `${entityRef}This record is the HUMAN USER's **observed experience** of ${personName} over time — not the Persona's own definition. Think of it as field notes from someone who has been talking with this Persona across many conversations.
123
+ descriptionSection = `This record is the HUMAN USER's **observed experience** of ${personName} over time. Think of it as field notes from someone who has been talking with this Persona across many conversations.
142
124
 
143
125
  ## Your job: add, never truncate
144
126
 
145
127
  The description is allowed to grow. **Never remove or summarize away existing content.**
146
128
 
147
129
  Add anything from the Most Recent Messages that:
148
- - Extends or nuances what's already known (new behaviors, new opinions, recurring themes)
149
- - Agrees with or contradicts the Persona's defined identity both are worth capturing
150
- - Reveals how the HUMAN USER experiences or relates to this Persona specifically
130
+ - Records a specific thing ${personName} said, did, or demonstrated in this conversation
131
+ - Captures how the HUMAN USER experienced or related to ${personName} in this specific exchange
132
+ - Notes behavior that stands out expected or surprising based on what the existing log already shows
151
133
 
152
134
  **Do NOT:**
153
135
  - Synthesize the existing description down to fewer sentences
154
136
  - Replace specific observations with vague summaries
155
137
  - Discard detail to "keep it brief" — brevity is wrong here
138
+ - Add anything that isn't directly observed in the Most Recent Messages
156
139
 
157
140
  If the new messages add nothing meaningful, return \`{}\`. Otherwise, return the **full updated description** — existing content preserved, new observations woven in.`;
158
141
 
@@ -25,10 +25,6 @@ interface BaseScanPromptData {
25
25
  persona_name: string;
26
26
  }
27
27
 
28
- export interface FactScanPromptData extends BaseScanPromptData {}
29
-
30
- export interface TraitScanPromptData extends BaseScanPromptData {}
31
-
32
28
  export interface TopicScanPromptData extends BaseScanPromptData {
33
29
  participant_context?: ParticipantContext;
34
30
  }
@@ -78,18 +74,6 @@ export interface TopicMatchPromptData {
78
74
  }>;
79
75
  }
80
76
 
81
- export interface PersonMatchPromptData {
82
- candidate_name: string;
83
- candidate_description: string;
84
- candidate_relationship: string;
85
- existing_people: Array<{
86
- id: string;
87
- name: string;
88
- description: string;
89
- relationship?: string;
90
- }>;
91
- }
92
-
93
77
  export interface FactScanResult {
94
78
  facts: FactScanCandidate[];
95
79
  }