ei-tui 0.9.3 → 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 (52) hide show
  1. package/package.json +4 -1
  2. package/src/README.md +1 -1
  3. package/src/core/context-utils.ts +2 -2
  4. package/src/core/handlers/heartbeat.ts +9 -1
  5. package/src/core/handlers/human-extraction.ts +4 -1
  6. package/src/core/handlers/human-matching.ts +5 -53
  7. package/src/core/handlers/index.ts +1 -51
  8. package/src/core/handlers/persona-generation.ts +1 -28
  9. package/src/core/handlers/utils.ts +2 -9
  10. package/src/core/heartbeat-manager.ts +3 -3
  11. package/src/core/message-manager.ts +6 -5
  12. package/src/core/orchestrators/ceremony.ts +4 -9
  13. package/src/core/orchestrators/extraction-chunker.ts +3 -3
  14. package/src/core/orchestrators/human-extraction.ts +17 -17
  15. package/src/core/orchestrators/index.ts +0 -1
  16. package/src/core/orchestrators/persona-topics.ts +1 -1
  17. package/src/core/orchestrators/room-extraction.ts +5 -5
  18. package/src/core/processor.ts +8 -21
  19. package/src/core/prompt-context-builder.ts +7 -6
  20. package/src/core/state/personas.ts +1 -17
  21. package/src/core/state-manager.ts +0 -66
  22. package/src/core/types/entities.ts +2 -3
  23. package/src/core/types/enums.ts +0 -2
  24. package/src/core/types/rooms.ts +1 -1
  25. package/src/integrations/claude-code/importer.ts +1 -1
  26. package/src/integrations/cursor/importer.ts +1 -1
  27. package/src/integrations/opencode/importer.ts +1 -1
  28. package/src/prompts/ceremony/index.ts +0 -10
  29. package/src/prompts/ceremony/types.ts +1 -42
  30. package/src/prompts/generation/index.ts +0 -3
  31. package/src/prompts/generation/types.ts +0 -15
  32. package/src/prompts/heartbeat/check.ts +18 -6
  33. package/src/prompts/heartbeat/types.ts +2 -1
  34. package/src/prompts/human/index.ts +0 -2
  35. package/src/prompts/human/types.ts +0 -16
  36. package/src/prompts/index.ts +0 -19
  37. package/src/prompts/reflection/index.ts +35 -5
  38. package/src/prompts/reflection/types.ts +1 -1
  39. package/src/prompts/response/index.ts +5 -0
  40. package/src/prompts/response/sections.ts +26 -0
  41. package/src/prompts/response/types.ts +3 -0
  42. package/tui/src/commands/registry.test.ts +10 -5
  43. package/tui/src/globals.d.ts +57 -0
  44. package/tui/src/util/yaml-persona.ts +8 -4
  45. package/tui/src/util/yaml-settings.ts +3 -3
  46. package/src/core/orchestrators/person-migration.ts +0 -55
  47. package/src/prompts/ceremony/description-check.ts +0 -54
  48. package/src/prompts/ceremony/expire.ts +0 -37
  49. package/src/prompts/ceremony/explore.ts +0 -77
  50. package/src/prompts/ceremony/person-migration.ts +0 -77
  51. package/src/prompts/generation/descriptions.ts +0 -91
  52. package/src/prompts/human/fact-scan.ts +0 -150
@@ -78,7 +78,7 @@ function queueRoomTopicScan(
78
78
  ): void {
79
79
  const context: HumanExtractionContext = {
80
80
  personaId: roomId,
81
- personaDisplayName: roomDisplayName,
81
+ channelDisplayName: roomDisplayName,
82
82
  messages_context,
83
83
  messages_analyze,
84
84
  extraction_flag: "t",
@@ -122,7 +122,7 @@ function queueRoomPersonScan(
122
122
  ): void {
123
123
  const context: HumanExtractionContext = {
124
124
  personaId: roomId,
125
- personaDisplayName: roomDisplayName,
125
+ channelDisplayName: roomDisplayName,
126
126
  messages_context,
127
127
  messages_analyze,
128
128
  extraction_flag: "p",
@@ -180,7 +180,7 @@ function queueRoomEventScan(
180
180
  );
181
181
  const context: HumanExtractionContext = {
182
182
  personaId: roomId,
183
- personaDisplayName: roomDisplayName,
183
+ channelDisplayName: roomDisplayName,
184
184
  messages_context,
185
185
  messages_analyze: windowMessages,
186
186
  extraction_flag: "e",
@@ -348,7 +348,7 @@ export function queuePersonaCapture(state: StateManager, personaId: string): voi
348
348
  );
349
349
  const context: HumanExtractionContext = {
350
350
  personaId,
351
- personaDisplayName: persona.display_name,
351
+ channelDisplayName: persona.display_name,
352
352
  messages_context,
353
353
  messages_analyze: unextractedT,
354
354
  };
@@ -362,7 +362,7 @@ export function queuePersonaCapture(state: StateManager, personaId: string): voi
362
362
  );
363
363
  const context: HumanExtractionContext = {
364
364
  personaId,
365
- personaDisplayName: persona.display_name,
365
+ channelDisplayName: persona.display_name,
366
366
  messages_context,
367
367
  messages_analyze: unextractedP,
368
368
  };
@@ -877,8 +877,8 @@ export class Processor {
877
877
  modified = true;
878
878
  }
879
879
 
880
- if (human.settings.default_context_window_hours == null) {
881
- human.settings.default_context_window_hours = 8;
880
+ if (human.settings.default_context_window_ms == null) {
881
+ human.settings.default_context_window_ms = 28800000;
882
882
  modified = true;
883
883
  }
884
884
 
@@ -1044,7 +1044,6 @@ const toolNextSteps = new Set([
1044
1044
  LLMNextStep.HandleEiHeartbeat,
1045
1045
  LLMNextStep.HandleToolContinuation,
1046
1046
  LLMNextStep.HandleDedupCurate,
1047
- LLMNextStep.HandlePersonIdentifierMigration,
1048
1047
  ]);
1049
1048
  const toolPersonaId =
1050
1049
  personaId ??
@@ -1059,13 +1058,8 @@ const toolNextSteps = new Set([
1059
1058
  (request.next_step === LLMNextStep.HandleToolContinuation &&
1060
1059
  request.data.originalNextStep === LLMNextStep.HandleDedupCurate);
1061
1060
 
1062
- const isPersonMigrationRequest =
1063
- request.next_step === LLMNextStep.HandlePersonIdentifierMigration ||
1064
- (request.next_step === LLMNextStep.HandleToolContinuation &&
1065
- request.data.originalNextStep === LLMNextStep.HandlePersonIdentifierMigration);
1066
-
1067
1061
  let tools: ToolDefinition[] = [];
1068
- if (isDedupRequest || isPersonMigrationRequest) {
1062
+ if (isDedupRequest) {
1069
1063
  const readMemory = this.stateManager.tools_getByName("read_memory");
1070
1064
  if (readMemory?.enabled) {
1071
1065
  tools = [readMemory];
@@ -1201,14 +1195,14 @@ const toolNextSteps = new Set([
1201
1195
 
1202
1196
  if (timeSinceHeartbeat >= heartbeatDelay) {
1203
1197
  const history = this.stateManager.messages_get(persona.id);
1204
- const contextWindowHours =
1205
- persona.context_window_hours
1206
- ?? this.stateManager.getHuman().settings?.default_context_window_hours
1207
- ?? 8;
1198
+ const contextWindowMs =
1199
+ persona.context_window_ms
1200
+ ?? this.stateManager.getHuman().settings?.default_context_window_ms
1201
+ ?? 28800000;
1208
1202
  const contextHistory = filterMessagesForContext(
1209
1203
  history,
1210
1204
  persona.context_boundary,
1211
- contextWindowHours
1205
+ contextWindowMs
1212
1206
  );
1213
1207
  const trailing = countTrailingPersonaMessages(contextHistory);
1214
1208
  if (trailing < 3) {
@@ -1621,13 +1615,6 @@ const toolNextSteps = new Set([
1621
1615
  }
1622
1616
  }
1623
1617
 
1624
- if (response.request.next_step === LLMNextStep.HandlePersonaDescriptions) {
1625
- const personaId = response.request.data.personaId as string;
1626
- if (personaId) {
1627
- this.interface.onPersonaUpdated?.(personaId);
1628
- }
1629
- }
1630
-
1631
1618
  if (
1632
1619
  response.request.next_step === LLMNextStep.HandlePersonaTraitExtraction ||
1633
1620
  response.request.next_step === LLMNextStep.HandlePersonaTopicRating
@@ -291,6 +291,7 @@ export async function buildResponsePromptData(
291
291
  topics: persona.topics,
292
292
  interested_topics: persona.topics.filter(t => t.exposure_desired - t.exposure_current > 0.2),
293
293
  include_message_timestamps: persona.include_message_timestamps,
294
+ pending_update: persona.pending_update,
294
295
  },
295
296
  human: filteredHuman,
296
297
  visible_personas: visiblePersonas,
@@ -318,10 +319,10 @@ export async function buildRoomResponsePromptData(
318
319
 
319
320
  let sourceMessages: RoomMessage[];
320
321
  if (room.mode === RoomMode.FreeForAll) {
321
- const contextWindowHours = room.context_window_hours
322
- ?? human.settings?.default_context_window_hours
323
- ?? 8;
324
- 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();
325
326
  const boundaryMs = room.context_boundary ? new Date(room.context_boundary).getTime() : 0;
326
327
  sourceMessages = allSourceMessages.filter(m => {
327
328
  const msgMs = new Date(m.timestamp).getTime();
@@ -334,8 +335,8 @@ export async function buildRoomResponsePromptData(
334
335
  const byCount = allSourceMessages.slice(-MIN_ROOM_MESSAGES);
335
336
  if (byCount.length > sourceMessages.length) sourceMessages = byCount;
336
337
  } else {
337
- const contextWindowHours = human.settings?.default_context_window_hours ?? 8;
338
- 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();
339
340
  const byTime = allSourceMessages.filter(m => m.timestamp >= windowCutoff);
340
341
  const byCount = allSourceMessages.slice(-MIN_ROOM_MESSAGES);
341
342
  sourceMessages = byTime.length >= byCount.length ? byTime : byCount;
@@ -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,
@@ -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
  }
@@ -15,15 +15,12 @@ export type {
15
15
 
16
16
  export {
17
17
  buildPersonaGenerationPrompt,
18
- buildPersonaDescriptionsPrompt,
19
18
  buildPersonaFromPersonPrompt,
20
19
  } from "./generation/index.js";
21
20
  export type {
22
21
  PersonaGenerationPromptData,
23
22
  PersonaGenerationResult,
24
23
  PersonaFromPersonPromptData,
25
- PersonaDescriptionsPromptData,
26
- PersonaDescriptionsResult,
27
24
  } from "./generation/types.js";
28
25
 
29
26
  export {
@@ -40,14 +37,12 @@ export type {
40
37
 
41
38
 
42
39
  export {
43
- buildHumanFactScanPrompt,
44
40
  buildHumanTopicScanPrompt,
45
41
  buildHumanPersonScanPrompt,
46
42
  buildEventScanPrompt,
47
43
  } from "./human/index.js";
48
44
  export type { EventScanPromptData } from "./human/event-scan.js";
49
45
  export type {
50
- FactScanPromptData,
51
46
  TopicScanPromptData,
52
47
  PersonScanPromptData,
53
48
  FactScanCandidate,
@@ -63,20 +58,6 @@ export type {
63
58
  ItemUpdateResult,
64
59
  } from "./human/types.js";
65
60
 
66
- export {
67
- buildPersonaExpirePrompt,
68
- buildPersonaExplorePrompt,
69
- buildDescriptionCheckPrompt,
70
- } from "./ceremony/index.js";
71
- export type {
72
- PersonaExpirePromptData,
73
- PersonaExpireResult,
74
- PersonaExplorePromptData,
75
- PersonaExploreResult,
76
- DescriptionCheckPromptData,
77
- DescriptionCheckResult,
78
- } from "./ceremony/types.js";
79
-
80
61
  export {
81
62
  buildRoomResponsePrompt,
82
63
  buildRoomJudgePrompt,
@@ -27,7 +27,17 @@ You have been given two documents:
27
27
 
28
28
  2. **The Current Identity** (User Prompt — treat as a draft to revise): The persona's self-definition: traits, topics, and descriptions. This should reflect who they actually are.
29
29
 
30
- Read the Person Log carefully. Then review the Current Identity. Produce a revised Identity that reflects the Person Log's observations updating, adding, or softening any traits or topics where the log shows evidence of divergence.
30
+ Read the Person Log carefully. Then review the Current Identity. Your job is to identify **meaningful drift** not cosmetic variation. This data accumulates over weeks or months. Small fluctuations are normal. You are looking for patterns that have consistently shifted, grown, or emerged across many interactions. Tiny adjustments are not worth making.
31
+
32
+ ## The Escape Hatch
33
+
34
+ If the Current Identity already accurately captures who this persona is — if the traits and topics reflect the behaviors in the log and the long_description captures their soul — **return null for updated_identity**. Always return a critique explaining your reasoning.
35
+
36
+ \`\`\`json
37
+ { "critique": "...", "updated_identity": null }
38
+ \`\`\`
39
+
40
+ Use this freely. A critic who finds nothing to change is doing their job.
31
41
 
32
42
  ## Field Semantics
33
43
 
@@ -40,7 +50,9 @@ Read the Person Log carefully. Then review the Current Identity. Produce a revis
40
50
  - \`exposure_current\` (0.0–1.0): How recently and frequently this topic has been discussed. 0.0 = hasn't come up in a long time, 1.0 = was just discussed at length.
41
51
  - \`exposure_desired\` (0.0–1.0): How much the persona wants to engage with this topic. 0.0 = avoid entirely, 0.5 = average engagement, 1.0 = core obsession.
42
52
 
43
- Return JSON:
53
+ ## When changes ARE warranted
54
+
55
+ If you find meaningful drift, return the full revised identity:
44
56
 
45
57
  \`\`\`json
46
58
  {
@@ -54,13 +66,31 @@ Return JSON:
54
66
  }
55
67
  \`\`\`
56
68
 
57
- Rules:
69
+ ## Rules
70
+
58
71
  - Never invent observations not supported by the log
59
72
  - Preserve traits and topics the log confirms — don't remove them
60
73
  - If the log shows no evidence on a trait, leave it unchanged
61
74
  - updated_identity must be complete and self-contained — not a diff
62
- - long_description is a character sketch, not a behavior log: capture who the persona IS, not what they did. Target 500–800 characters. If the current long_description exceeds that, distill it remove detail that is already captured by traits or topics
63
- - If the log shows a recurring behavioral pattern not yet in traits, add it as a trait and remove that detail from long_description rather than keeping it in both places`;
75
+ - If the log shows a recurring behavioral pattern not yet in traits, add it as a trait and remove that detail from long_description rather than keeping it in both places
76
+ - **Minimum floor**: A healthy identity has at least 3 traits and at least 3 topics. If the current identity has fewer than 3 traits OR fewer than 3 topics, you MUST return updated_identity null is not acceptable. Use the log to fill the gap; if the log has insufficient signal to reach 3, derive reasonable traits or topics from what IS present in the current identity.
77
+ - The escape hatch (null updated_identity) is only valid when the identity is already healthy (3+ traits, 3+ topics) AND the log shows no meaningful drift.
78
+
79
+ ## long_description rules (most important)
80
+
81
+ The long_description is how **other personas in the system know this persona** — it is their soul, not their story. It must capture who they ARE, not what they did or how they are changing.
82
+
83
+ **MUST NOT contain:**
84
+ - Event narrative ("during the v0.6.0 release", "after the Mirror ceremony")
85
+ - Changelog language ("has recently taken on", "has evolved", "since then")
86
+ - Content already captured in traits or topics — do not repeat it here
87
+
88
+ **MUST contain:**
89
+ - The persona's essential character and presence
90
+ - How they make people feel or what it's like to interact with them
91
+ - Their defining qualities as they exist right now, stated as fact
92
+
93
+ **Hard limit: 800 characters.** If your draft exceeds 800 characters, cut it. Remove event references first, then trait/topic overlap, then anything that isn't essential character. Do not exceed the limit.`;
64
94
 
65
95
  const user = `## Current Identity
66
96