ei-tui 0.1.24 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +42 -0
  2. package/package.json +1 -1
  3. package/src/README.md +4 -11
  4. package/src/cli/README.md +4 -5
  5. package/src/cli/retrieval.ts +3 -25
  6. package/src/cli.ts +3 -7
  7. package/src/core/AGENTS.md +1 -1
  8. package/src/core/constants/built-in-facts.ts +49 -0
  9. package/src/core/constants/index.ts +1 -0
  10. package/src/core/context-utils.ts +0 -1
  11. package/src/core/embedding-service.ts +8 -0
  12. package/src/core/handlers/dedup.ts +34 -14
  13. package/src/core/handlers/heartbeat.ts +2 -3
  14. package/src/core/handlers/human-extraction.ts +95 -30
  15. package/src/core/handlers/human-matching.ts +326 -248
  16. package/src/core/handlers/index.ts +8 -6
  17. package/src/core/handlers/persona-generation.ts +8 -8
  18. package/src/core/handlers/rewrite.ts +4 -29
  19. package/src/core/handlers/utils.ts +23 -1
  20. package/src/core/heartbeat-manager.ts +2 -4
  21. package/src/core/human-data-manager.ts +5 -27
  22. package/src/core/message-manager.ts +10 -10
  23. package/src/core/orchestrators/ceremony.ts +60 -46
  24. package/src/core/orchestrators/dedup-phase.ts +11 -5
  25. package/src/core/orchestrators/human-extraction.ts +351 -207
  26. package/src/core/orchestrators/index.ts +6 -4
  27. package/src/core/orchestrators/persona-generation.ts +3 -3
  28. package/src/core/processor.ts +113 -22
  29. package/src/core/prompt-context-builder.ts +4 -6
  30. package/src/core/state/human.ts +1 -26
  31. package/src/core/state/personas.ts +2 -2
  32. package/src/core/state-manager.ts +107 -14
  33. package/src/core/tools/builtin/read-memory.ts +7 -8
  34. package/src/core/types/data-items.ts +2 -4
  35. package/src/core/types/entities.ts +6 -4
  36. package/src/core/types/enums.ts +6 -9
  37. package/src/core/types/llm.ts +2 -2
  38. package/src/core/utils/crossFind.ts +2 -5
  39. package/src/core/utils/event-windows.ts +31 -0
  40. package/src/integrations/claude-code/importer.ts +8 -4
  41. package/src/integrations/claude-code/types.ts +2 -0
  42. package/src/integrations/opencode/importer.ts +7 -3
  43. package/src/prompts/AGENTS.md +73 -1
  44. package/src/prompts/ceremony/dedup.ts +41 -7
  45. package/src/prompts/ceremony/rewrite.ts +3 -22
  46. package/src/prompts/ceremony/types.ts +3 -3
  47. package/src/prompts/generation/descriptions.ts +2 -2
  48. package/src/prompts/generation/types.ts +2 -2
  49. package/src/prompts/heartbeat/types.ts +2 -2
  50. package/src/prompts/human/event-scan.ts +122 -0
  51. package/src/prompts/human/fact-find.ts +106 -0
  52. package/src/prompts/human/fact-scan.ts +0 -2
  53. package/src/prompts/human/index.ts +17 -10
  54. package/src/prompts/human/person-match.ts +65 -0
  55. package/src/prompts/human/person-scan.ts +52 -59
  56. package/src/prompts/human/person-update.ts +241 -0
  57. package/src/prompts/human/topic-match.ts +65 -0
  58. package/src/prompts/human/topic-scan.ts +51 -71
  59. package/src/prompts/human/topic-update.ts +295 -0
  60. package/src/prompts/human/types.ts +63 -40
  61. package/src/prompts/index.ts +4 -8
  62. package/src/prompts/persona/topics-update.ts +2 -2
  63. package/src/prompts/persona/traits.ts +2 -2
  64. package/src/prompts/persona/types.ts +3 -3
  65. package/src/prompts/response/index.ts +1 -1
  66. package/src/prompts/response/sections.ts +9 -12
  67. package/src/prompts/response/types.ts +2 -3
  68. package/src/storage/embeddings.ts +1 -1
  69. package/src/storage/index.ts +1 -0
  70. package/src/storage/indexed.ts +174 -0
  71. package/src/storage/merge.ts +67 -2
  72. package/tui/src/app.tsx +7 -5
  73. package/tui/src/commands/archive.tsx +2 -2
  74. package/tui/src/commands/context.tsx +3 -4
  75. package/tui/src/commands/delete.tsx +4 -4
  76. package/tui/src/commands/dlq.ts +3 -4
  77. package/tui/src/commands/help.tsx +1 -1
  78. package/tui/src/commands/me.tsx +8 -18
  79. package/tui/src/commands/persona.tsx +2 -2
  80. package/tui/src/commands/provider.tsx +3 -5
  81. package/tui/src/commands/queue.ts +3 -4
  82. package/tui/src/commands/quotes.tsx +6 -8
  83. package/tui/src/commands/registry.ts +1 -1
  84. package/tui/src/commands/setsync.tsx +2 -2
  85. package/tui/src/commands/settings.tsx +18 -4
  86. package/tui/src/commands/spotify-auth.ts +0 -1
  87. package/tui/src/commands/tools.tsx +4 -5
  88. package/tui/src/context/ei.tsx +5 -14
  89. package/tui/src/context/overlay.tsx +17 -6
  90. package/tui/src/util/editor.ts +22 -11
  91. package/tui/src/util/persona-editor.tsx +6 -8
  92. package/tui/src/util/provider-editor.tsx +6 -8
  93. package/tui/src/util/toolkit-editor.tsx +3 -4
  94. package/tui/src/util/yaml-serializers.ts +48 -33
  95. package/src/cli/commands/traits.ts +0 -25
  96. package/src/prompts/human/item-match.ts +0 -74
  97. package/src/prompts/human/item-update.ts +0 -364
  98. package/src/prompts/human/trait-scan.ts +0 -115
@@ -14,8 +14,8 @@ import {
14
14
  handlePersonaTopicMatch,
15
15
  handlePersonaTopicUpdate,
16
16
  } from "./persona-topics.js";
17
- import { handleHumanFactScan, handleHumanTraitScan, handleHumanTopicScan, handleHumanPersonScan } from "./human-extraction.js";
18
- import { handleHumanItemMatch, handleHumanItemUpdate } from "./human-matching.js";
17
+ import { handleFactFind, handleHumanTopicScan, handleHumanPersonScan, handleEventScan } from "./human-extraction.js";
18
+ import { handleTopicMatch, handleTopicUpdate, handlePersonMatch, handlePersonUpdate } from "./human-matching.js";
19
19
  import { handleRewriteScan, handleRewriteRewrite } from "./rewrite.js";
20
20
  import { handleDedupCurate } from "./dedup.js";
21
21
 
@@ -23,12 +23,13 @@ export const handlers: Record<LLMNextStep, ResponseHandler> = {
23
23
  handlePersonaResponse,
24
24
  handlePersonaGeneration,
25
25
  handlePersonaDescriptions,
26
- handleHumanFactScan,
27
- handleHumanTraitScan,
26
+ handleFactFind,
28
27
  handleHumanTopicScan,
29
28
  handleHumanPersonScan,
30
- handleHumanItemMatch,
31
- handleHumanItemUpdate,
29
+ handleTopicMatch,
30
+ handleTopicUpdate,
31
+ handlePersonMatch,
32
+ handlePersonUpdate,
32
33
  handlePersonaTraitExtraction,
33
34
  handlePersonaTopicScan,
34
35
  handlePersonaTopicMatch,
@@ -43,4 +44,5 @@ export const handlers: Record<LLMNextStep, ResponseHandler> = {
43
44
  handleRewriteScan,
44
45
  handleRewriteRewrite,
45
46
  handleDedupCurate,
47
+ handleEventScan,
46
48
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  type LLMResponse,
3
- type Trait,
3
+ type PersonaTrait,
4
4
  type PersonaTopic,
5
5
  } from "../types.js";
6
6
  import type { StateManager } from "../state-manager.js";
@@ -27,10 +27,10 @@ export function handlePersonaGeneration(response: LLMResponse, state: StateManag
27
27
  (existingPartial.traits ?? []).filter(t => t.name?.trim()).map(t => [t.name!.toLowerCase().trim(), t])
28
28
  );
29
29
 
30
- const mergedLlmTraits: Trait[] = (result?.traits || []).map(t => {
30
+ const mergedLlmTraits: PersonaTrait[] = (result?.traits || []).map(t => {
31
31
  const userTrait = userTraitsByName.get(t.name?.toLowerCase().trim() ?? '');
32
32
  return {
33
- id: (userTrait as Trait | undefined)?.id ?? crypto.randomUUID(),
33
+ id: (userTrait as PersonaTrait | undefined)?.id ?? crypto.randomUUID(),
34
34
  name: t.name,
35
35
  description: userTrait?.description?.trim() || t.description,
36
36
  sentiment: userTrait?.sentiment ?? t.sentiment ?? 0,
@@ -41,10 +41,10 @@ export function handlePersonaGeneration(response: LLMResponse, state: StateManag
41
41
 
42
42
  // Keep user-provided traits the LLM didn't return
43
43
  const llmTraitNames = new Set(mergedLlmTraits.map(t => t.name?.toLowerCase().trim()));
44
- const preservedUserTraits: Trait[] = (existingPartial.traits ?? [])
44
+ const preservedUserTraits: PersonaTrait[] = (existingPartial.traits ?? [])
45
45
  .filter(t => t.name?.trim() && !llmTraitNames.has(t.name.toLowerCase().trim()))
46
46
  .map(t => ({
47
- id: (t as Trait).id ?? crypto.randomUUID(),
47
+ id: (t as PersonaTrait).id ?? crypto.randomUUID(),
48
48
  name: t.name!,
49
49
  description: t.description || '',
50
50
  sentiment: t.sentiment ?? 0,
@@ -52,9 +52,9 @@ export function handlePersonaGeneration(response: LLMResponse, state: StateManag
52
52
  last_updated: now,
53
53
  }));
54
54
 
55
- const mergedTraits: Trait[] = mergedLlmTraits.length > 0
55
+ const mergedTraits: PersonaTrait[] = mergedLlmTraits.length > 0
56
56
  ? [...mergedLlmTraits, ...preservedUserTraits]
57
- : (existingPartial.traits as Trait[] | undefined) ?? [];
57
+ : (existingPartial.traits as PersonaTrait[] | undefined) ?? [];
58
58
 
59
59
  // Merge LLM topics into user-provided topics by name.
60
60
  // User-provided fields win; LLM fills in what the user left blank.
@@ -151,7 +151,7 @@ export function handlePersonaTraitExtraction(response: LLMResponse, state: State
151
151
  }
152
152
 
153
153
  const now = new Date().toISOString();
154
- const traits: Trait[] = result.map(t => ({
154
+ const traits: PersonaTrait[] = result.map(t => ({
155
155
  id: crypto.randomUUID(),
156
156
  name: t.name,
157
157
  description: t.description,
@@ -2,10 +2,8 @@ import {
2
2
  LLMRequestType,
3
3
  LLMPriority,
4
4
  LLMNextStep,
5
- ValidationLevel,
6
5
  type LLMResponse,
7
6
  type Fact,
8
- type Trait,
9
7
  type Topic,
10
8
  type Person,
11
9
  type DataItemBase,
@@ -48,7 +46,7 @@ export async function handleRewriteScan(response: LLMResponse, state: StateManag
48
46
  // Re-read the item from current state (it may have changed since scan was queued)
49
47
  const human = state.getHuman();
50
48
  const allItems: DataItemBase[] = [
51
- ...human.facts, ...human.traits, ...human.topics, ...human.people,
49
+ ...human.facts, ...human.topics, ...human.people,
52
50
  ];
53
51
  const currentItem = allItems.find(i => i.id === itemId);
54
52
  if (!currentItem) {
@@ -61,11 +59,11 @@ export async function handleRewriteScan(response: LLMResponse, state: StateManag
61
59
  for (const searchTerm of subjects) {
62
60
  try {
63
61
  const results = await searchHumanData(state, searchTerm, {
64
- types: ["fact", "trait", "topic", "person"],
62
+ types: ["fact", "topic", "person"],
65
63
  limit: 4, // fetch 4 so we can exclude original and still have 3
66
64
  });
67
65
  const allMatches: DataItemBase[] = [
68
- ...results.facts, ...results.traits, ...results.topics, ...results.people,
66
+ ...results.facts, ...results.topics, ...results.people,
69
67
  ].filter(m => m.id !== itemId); // exclude original
70
68
  subjectMatches.push({ searchTerm, matches: allMatches.slice(0, 3) });
71
69
  } catch (err) {
@@ -122,7 +120,7 @@ export async function handleRewriteRewrite(response: LLMResponse, state: StateMa
122
120
 
123
121
  // Look up the original item to inherit persona_groups
124
122
  const allItems: DataItemBase[] = [
125
- ...human.facts, ...human.traits, ...human.topics, ...human.people,
123
+ ...human.facts, ...human.topics, ...human.people,
126
124
  ];
127
125
  const originalItem = allItems.find(i => i.id === itemId);
128
126
  const inheritedGroups = originalItem?.persona_groups;
@@ -130,7 +128,6 @@ export async function handleRewriteRewrite(response: LLMResponse, state: StateMa
130
128
  // Helper: resolve actual type from existing records (don't trust LLM's type field)
131
129
  const resolveExistingType = (id: string): RewriteItemType | null => {
132
130
  if (human.facts.find(f => f.id === id)) return "fact";
133
- if (human.traits.find(t => t.id === id)) return "trait";
134
131
  if (human.topics.find(t => t.id === id)) return "topic";
135
132
  if (human.people.find(p => p.id === id)) return "person";
136
133
  return null;
@@ -175,19 +172,6 @@ export async function handleRewriteRewrite(response: LLMResponse, state: StateMa
175
172
  });
176
173
  break;
177
174
  }
178
- case "trait": {
179
- const existing = human.traits.find(t => t.id === item.id)!;
180
- state.human_trait_upsert({
181
- ...existing,
182
- name: item.name,
183
- description: item.description,
184
- sentiment: item.sentiment ?? existing.sentiment,
185
- strength: item.strength ?? existing.strength,
186
- last_updated: now,
187
- embedding,
188
- });
189
- break;
190
- }
191
175
  case "topic": {
192
176
  const existing = human.topics.find(t => t.id === item.id)!;
193
177
  state.human_topic_upsert({
@@ -247,20 +231,11 @@ export async function handleRewriteRewrite(response: LLMResponse, state: StateMa
247
231
  case "fact": {
248
232
  const fact: Fact = {
249
233
  ...baseFields,
250
- validated: ValidationLevel.None,
251
234
  validated_date: now,
252
235
  };
253
236
  state.human_fact_upsert(fact);
254
237
  break;
255
238
  }
256
- case "trait": {
257
- const trait: Trait = {
258
- ...baseFields,
259
- strength: item.strength ?? 0.5,
260
- };
261
- state.human_trait_upsert(trait);
262
- break;
263
- }
264
239
  case "topic": {
265
240
  if (!item.category) {
266
241
  console.warn(`[handleRewriteRewrite] New topic "${item.name}" missing category — defaulting to "Interest"`);
@@ -1,7 +1,29 @@
1
1
  import type { Message, LLMResponse } from "../types.js";
2
2
  import type { StateManager } from "../state-manager.js";
3
3
 
4
- export type ExtractionFlag = "f" | "r" | "p" | "o";
4
+ export function resolveMessageWindow(
5
+ response: LLMResponse,
6
+ state: StateManager
7
+ ): { messages_context: Message[]; messages_analyze: Message[] } {
8
+ const personaId = response.request.data.personaId as string;
9
+ const messageIdsToMark = response.request.data.message_ids_to_mark as string[] | undefined;
10
+ const allMessages = state.messages_get(personaId);
11
+
12
+ if (messageIdsToMark && messageIdsToMark.length > 0) {
13
+ const messageIdSet = new Set(messageIdsToMark);
14
+ const messages_analyze = allMessages.filter(m => messageIdSet.has(m.id));
15
+ const analyzeStartTime = messages_analyze[0]?.timestamp ?? '9999';
16
+ const messages_context = allMessages.filter(m =>
17
+ !messageIdSet.has(m.id) && new Date(m.timestamp).getTime() < new Date(analyzeStartTime).getTime()
18
+ );
19
+ return { messages_context, messages_analyze };
20
+ }
21
+
22
+ const analyzeFrom = response.request.data.analyze_from_timestamp as string | null;
23
+ return splitMessagesByTimestamp(allMessages, analyzeFrom);
24
+ }
25
+
26
+ export type ExtractionFlag = "f" | "t" | "p" | "e";
5
27
 
6
28
  export function splitMessagesByTimestamp(
7
29
  messages: Message[],
@@ -2,7 +2,6 @@ import {
2
2
  LLMRequestType,
3
3
  LLMPriority,
4
4
  LLMNextStep,
5
- ValidationLevel,
6
5
  type HumanEntity,
7
6
  type Message,
8
7
  } from "./types.js";
@@ -72,9 +71,8 @@ export async function queueEiHeartbeat(
72
71
  const unverifiedFacts = human.facts
73
72
  .filter(
74
73
  (f) =>
75
- f.validated === ValidationLevel.None &&
76
- f.learned_by !== "ei" &&
77
- (f.last_changed_by === undefined || f.last_changed_by !== "ei")
74
+ f.description !== '' &&
75
+ f.validated_date === ''
78
76
  )
79
77
  .slice(0, 5);
80
78
  for (const fact of unverifiedFacts) {
@@ -1,4 +1,4 @@
1
- import type { HumanEntity, Fact, Trait, Topic, Person, Quote } from "./types.js";
1
+ import type { HumanEntity, Fact, Topic, Person, Quote } from "./types.js";
2
2
  import { StateManager } from "./state-manager.js";
3
3
  import {
4
4
  getEmbeddingService,
@@ -24,7 +24,7 @@ export async function updateHuman(sm: StateManager, updates: Partial<HumanEntity
24
24
  }
25
25
 
26
26
  // =============================================================================
27
- // FACTS / TRAITS / TOPICS / PEOPLE UPSERT
27
+ // FACTS / TOPICS / PEOPLE UPSERT
28
28
  // =============================================================================
29
29
 
30
30
  export async function upsertFact(sm: StateManager, fact: Fact): Promise<void> {
@@ -40,18 +40,6 @@ export async function upsertFact(sm: StateManager, fact: Fact): Promise<void> {
40
40
  sm.human_fact_upsert(fact);
41
41
  }
42
42
 
43
- export async function upsertTrait(sm: StateManager, trait: Trait): Promise<void> {
44
- const human = sm.getHuman();
45
- const existing = human.traits.find((t) => t.id === trait.id);
46
-
47
- if (needsEmbeddingUpdate(existing, trait)) {
48
- trait.embedding = await computeDataItemEmbedding(trait);
49
- } else if (existing?.embedding) {
50
- trait.embedding = existing.embedding;
51
- }
52
-
53
- sm.human_trait_upsert(trait);
54
- }
55
43
 
56
44
  export async function upsertTopic(sm: StateManager, topic: Topic): Promise<void> {
57
45
  const human = sm.getHuman();
@@ -81,16 +69,13 @@ export async function upsertPerson(sm: StateManager, person: Person): Promise<vo
81
69
 
82
70
  export async function removeDataItem(
83
71
  sm: StateManager,
84
- type: "fact" | "trait" | "topic" | "person",
72
+ type: "fact" | "topic" | "person",
85
73
  id: string
86
74
  ): Promise<void> {
87
75
  switch (type) {
88
76
  case "fact":
89
77
  sm.human_fact_remove(id);
90
78
  break;
91
- case "trait":
92
- sm.human_trait_remove(id);
93
- break;
94
79
  case "topic":
95
80
  sm.human_topic_remove(id);
96
81
  break;
@@ -160,21 +145,19 @@ export async function getQuotesForMessage(sm: StateManager, messageId: string):
160
145
  export async function searchHumanData(
161
146
  sm: StateManager,
162
147
  query: string,
163
- options: { types?: Array<"fact" | "trait" | "topic" | "person" | "quote">; limit?: number } = {}
148
+ options: { types?: Array<"fact" | "topic" | "person" | "quote">; limit?: number } = {}
164
149
  ): Promise<{
165
150
  facts: Fact[];
166
- traits: Trait[];
167
151
  topics: Topic[];
168
152
  people: Person[];
169
153
  quotes: Quote[];
170
154
  }> {
171
- const { types = ["fact", "trait", "topic", "person", "quote"], limit = 10 } = options;
155
+ const { types = ["fact", "topic", "person", "quote"], limit = 10 } = options;
172
156
  const human = sm.getHuman();
173
157
  const SIMILARITY_THRESHOLD = 0.3;
174
158
 
175
159
  const result = {
176
160
  facts: [] as Fact[],
177
- traits: [] as Trait[],
178
161
  topics: [] as Topic[],
179
162
  people: [] as Person[],
180
163
  quotes: [] as Quote[],
@@ -211,11 +194,6 @@ export async function searchHumanData(
211
194
  stripDataItemEmbedding
212
195
  );
213
196
  }
214
- if (types.includes("trait")) {
215
- result.traits = searchItems(human.traits, (t) => `${t.name} ${t.description || ""}`).map(
216
- stripDataItemEmbedding
217
- );
218
- }
219
197
  if (types.includes("topic")) {
220
198
  result.topics = searchItems(human.topics, (t) => `${t.name} ${t.description || ""}`).map(
221
199
  stripDataItemEmbedding
@@ -16,7 +16,7 @@ import {
16
16
  } from "../prompts/index.js";
17
17
  import { buildResponsePromptData } from "./prompt-context-builder.js";
18
18
  import {
19
- queueFactScan,
19
+ queueFactFind,
20
20
  queueTopicScan,
21
21
  queuePersonScan,
22
22
  type ExtractionContext,
@@ -178,7 +178,7 @@ export async function sendMessage(
178
178
  const traitExtractionData: PersonaTraitExtractionPromptData = {
179
179
  persona_name: persona.display_name,
180
180
  current_traits: persona.traits,
181
- messages_context: history.slice(0, -1),
181
+ messages_context: history.slice(-11, -1),
182
182
  messages_analyze: [message],
183
183
  };
184
184
  const traitPrompt = buildPersonaTraitExtractionPrompt(traitExtractionData);
@@ -217,7 +217,7 @@ export function checkAndQueueHumanExtraction(
217
217
  const human = sm.getHuman();
218
218
 
219
219
  const unextractedFacts = sm.messages_getUnextracted(personaId, "f");
220
- const factsThreshold = Math.min(EXTRACTION_TAPER_CAP, human.facts.length);
220
+ const factsThreshold = Math.min(EXTRACTION_TAPER_CAP, human.facts.filter(f => f.description && f.description !== "").length);
221
221
  if (unextractedFacts.length > 0 && unextractedFacts.length >= factsThreshold) {
222
222
  const context: ExtractionContext = {
223
223
  personaId,
@@ -226,21 +226,21 @@ export function checkAndQueueHumanExtraction(
226
226
  messages_analyze: unextractedFacts,
227
227
  extraction_flag: "f",
228
228
  };
229
- queueFactScan(context, sm);
229
+ queueFactFind(context, sm);
230
230
  console.log(
231
231
  `[Processor] Human Seed extraction: facts (threshold: ${factsThreshold}, unextracted: ${unextractedFacts.length})`
232
232
  );
233
233
  }
234
234
 
235
- const unextractedTopics = sm.messages_getUnextracted(personaId, "p");
235
+ const unextractedTopics = sm.messages_getUnextracted(personaId, "t");
236
236
  const topicsThreshold = Math.min(EXTRACTION_TAPER_CAP, human.topics.length);
237
237
  if (unextractedTopics.length > 0 && unextractedTopics.length >= topicsThreshold) {
238
238
  const context: ExtractionContext = {
239
239
  personaId,
240
240
  personaDisplayName,
241
- messages_context: history.filter((m) => m.p === true),
241
+ messages_context: history.filter((m) => m.t === true),
242
242
  messages_analyze: unextractedTopics,
243
- extraction_flag: "p",
243
+ extraction_flag: "t",
244
244
  };
245
245
  queueTopicScan(context, sm);
246
246
  console.log(
@@ -248,15 +248,15 @@ export function checkAndQueueHumanExtraction(
248
248
  );
249
249
  }
250
250
 
251
- const unextractedPeople = sm.messages_getUnextracted(personaId, "o");
251
+ const unextractedPeople = sm.messages_getUnextracted(personaId, "p");
252
252
  const peopleThreshold = Math.min(EXTRACTION_TAPER_CAP, human.people.length);
253
253
  if (unextractedPeople.length > 0 && unextractedPeople.length >= peopleThreshold) {
254
254
  const context: ExtractionContext = {
255
255
  personaId,
256
256
  personaDisplayName,
257
- messages_context: history.filter((m) => m.o === true),
257
+ messages_context: history.filter((m) => m.p === true),
258
258
  messages_analyze: unextractedPeople,
259
- extraction_flag: "o",
259
+ extraction_flag: "p",
260
260
  };
261
261
  queuePersonScan(context, sm);
262
262
  console.log(
@@ -2,10 +2,10 @@ import { LLMRequestType, LLMPriority, LLMNextStep, MESSAGE_MIN_COUNT, MESSAGE_MA
2
2
  import type { StateManager } from "../state-manager.js";
3
3
  import { applyDecayToValue } from "../utils/index.js";
4
4
  import {
5
- queueFactScan,
6
- queueTraitScan,
5
+ queueFactFind,
7
6
  queueTopicScan,
8
7
  queuePersonScan,
8
+ queueEventSummary,
9
9
  type ExtractionContext,
10
10
  type ExtractionOptions,
11
11
  } from "./human-extraction.js";
@@ -109,56 +109,45 @@ function queueExposurePhase(personaId: string, state: StateManager, options?: Ex
109
109
  messages_analyze: unextractedFacts,
110
110
  extraction_flag: "f",
111
111
  };
112
- queueFactScan(context, state, options);
112
+ queueFactFind(context, state, options);
113
113
  }
114
114
 
115
- const unextractedTraits = state.messages_getUnextracted(personaId, "r");
116
- if (unextractedTraits.length > 0) {
117
- const context: ExtractionContext = {
118
- personaId,
119
- personaDisplayName: persona.display_name,
120
- messages_context: allMessages.filter(m => m.r === true),
121
- messages_analyze: unextractedTraits,
122
- extraction_flag: "r",
123
- };
124
- queueTraitScan(context, state, options);
125
- }
126
115
 
127
- const unextractedTopics = state.messages_getUnextracted(personaId, "p");
116
+ const unextractedTopics = state.messages_getUnextracted(personaId, "t");
128
117
  if (unextractedTopics.length > 0) {
129
118
  const context: ExtractionContext = {
130
119
  personaId,
131
120
  personaDisplayName: persona.display_name,
132
- messages_context: allMessages.filter(m => m.p === true),
121
+ messages_context: allMessages.filter(m => m.t === true),
133
122
  messages_analyze: unextractedTopics,
134
- extraction_flag: "p",
123
+ extraction_flag: "t",
135
124
  };
136
125
  queueTopicScan(context, state, options);
137
126
  }
138
127
 
139
- const unextractedPeople = state.messages_getUnextracted(personaId, "o");
128
+ const unextractedPeople = state.messages_getUnextracted(personaId, "p");
140
129
  if (unextractedPeople.length > 0) {
141
130
  const context: ExtractionContext = {
142
131
  personaId,
143
132
  personaDisplayName: persona.display_name,
144
- messages_context: allMessages.filter(m => m.o === true),
133
+ messages_context: allMessages.filter(m => m.p === true),
145
134
  messages_analyze: unextractedPeople,
146
- extraction_flag: "o",
135
+ extraction_flag: "p",
147
136
  };
148
137
  queuePersonScan(context, state, options);
149
138
  }
150
139
 
151
- const totalUnextracted = unextractedFacts.length + unextractedTraits.length + unextractedTopics.length + unextractedPeople.length;
140
+ const totalUnextracted = unextractedFacts.length + unextractedTopics.length + unextractedPeople.length;
152
141
  if (totalUnextracted > 0) {
153
- console.log(`[ceremony:exposure] Queued human extraction scans (f:${unextractedFacts.length}, r:${unextractedTraits.length}, p:${unextractedTopics.length}, o:${unextractedPeople.length})`);
142
+ console.log(`[ceremony:exposure] Queued human extraction scans (f:${unextractedFacts.length}, t:${unextractedTopics.length}, p:${unextractedPeople.length})`);
154
143
  }
155
144
 
156
- const unextractedForPersonaTopics = state.messages_getUnextracted(personaId, "p");
145
+ const unextractedForPersonaTopics = state.messages_getUnextracted(personaId, "t");
157
146
  if (unextractedForPersonaTopics.length > 0) {
158
147
  const personaTopicContext: PersonaTopicContext = {
159
148
  personaId,
160
149
  personaDisplayName: persona.display_name,
161
- messages_context: allMessages.filter(m => m.p === true),
150
+ messages_context: allMessages.filter(m => m.t === true),
162
151
  messages_analyze: unextractedForPersonaTopics,
163
152
  };
164
153
  queuePersonaTopicScan(personaTopicContext, state);
@@ -171,7 +160,7 @@ function queueExposurePhase(personaId: string, state: StateManager, options?: Ex
171
160
  * AND at the end of startCeremony (for the zero-messages edge case).
172
161
  *
173
162
  * If any ceremony_progress items remain in the queue, does nothing — more work pending.
174
- * If the queue is clear of ceremony items, advances to DecayPrune → Expire.
163
+ * Phase 1: Dedup Phase 2: Expose Phase 3: EventSummaryDecay → Expire
175
164
  */
176
165
  export function handleCeremonyProgress(state: StateManager, lastPhase: number): void {
177
166
  if (state.queue_hasPendingCeremonies()) {
@@ -182,7 +171,6 @@ export function handleCeremonyProgress(state: StateManager, lastPhase: number):
182
171
  // Dedup phase complete → start Expose phase
183
172
  console.log("[ceremony:progress] Dedup complete, starting Expose phase");
184
173
 
185
- const human = state.getHuman();
186
174
  const personas = state.persona_getAll();
187
175
  const activePersonas = personas.filter(p =>
188
176
  !p.is_paused &&
@@ -190,24 +178,40 @@ export function handleCeremonyProgress(state: StateManager, lastPhase: number):
190
178
  !p.is_static
191
179
  );
192
180
 
193
- const lastCeremony = human.settings?.ceremony?.last_ceremony
194
- ? new Date(human.settings.ceremony.last_ceremony).getTime()
195
- : 0;
196
-
197
- const personasWithActivity = activePersonas.filter(p => {
198
- const lastActivity = p.last_activity ? new Date(p.last_activity).getTime() : 0;
199
- return lastActivity > lastCeremony;
181
+ // Find personas with unprocessed messages (any message with p/r/o/f = false)
182
+ const personasWithUnprocessed = activePersonas.filter(p => {
183
+ const messages = state.messages_get(p.id);
184
+ return messages.some(msg =>
185
+ !msg.t ||
186
+ !msg.p ||
187
+ !msg.f
188
+ );
200
189
  });
201
190
 
191
+ console.log(`[ceremony:expose] Found ${activePersonas.length} active personas, ${personasWithUnprocessed.length} with unprocessed messages`);
192
+
202
193
  const options: ExtractionOptions = { ceremony_progress: 2 };
203
- for (const persona of personasWithActivity) {
194
+ for (const persona of personasWithUnprocessed) {
204
195
  queueExposurePhase(persona.id, state, options);
205
196
  }
206
197
  return;
207
198
  }
199
+
200
+ if (lastPhase === 2) {
201
+ console.log("[ceremony:progress] Expose complete, starting EventSummary phase");
202
+ const options: ExtractionOptions = { ceremony_progress: 3 };
203
+ queueEventSummaryForAll(state, options);
204
+
205
+ // Zero-work guard: same pattern as DeDupe phase
206
+ if (!state.queue_hasPendingCeremonies()) {
207
+ console.log("[ceremony:progress] No event summary work, advancing to Decay");
208
+ handleCeremonyProgress(state, 3);
209
+ }
210
+ return;
211
+ }
208
212
 
209
- // Phase 2 (Expose) complete → advance to Decay/Prune/Expire/Explore
210
- console.log("[ceremony:progress] All exposure scans complete, advancing to Decay");
213
+ // Phase 3 (EventSummary) complete → advance to Decay/Prune/Expire/Explore
214
+ console.log("[ceremony:progress] EventSummary complete, advancing to Decay");
211
215
 
212
216
  const personas = state.persona_getAll();
213
217
  const activePersonas = personas.filter(p =>
@@ -233,7 +237,7 @@ export function handleCeremonyProgress(state: StateManager, lastPhase: number):
233
237
  // Human ceremony: decay topics + people
234
238
  runHumanCeremony(state);
235
239
 
236
- // Dedup phase: log near-duplicate human entities (dry-run only, no mutations)
240
+ // Dedup phase: log near-duplicate human entity candidates for visibility
237
241
  runDedupPhase(state);
238
242
 
239
243
  // Rewrite phase: fire-and-forget scans for bloated human data items
@@ -317,7 +321,7 @@ export function prunePersonaMessages(personaId: string, state: StateManager): vo
317
321
  const msgMs = new Date(m.timestamp).getTime();
318
322
  if (msgMs >= cutoffMs) break; // Sorted by time, no more old ones
319
323
 
320
- const fullyExtracted = m.p && m.r && m.o && m.f;
324
+ const fullyExtracted = m.t && m.p && m.f; // r intentionally excluded — trait extraction deprecated
321
325
  if (fullyExtracted) {
322
326
  toRemove.push(m.id);
323
327
  }
@@ -515,7 +519,7 @@ export function runHumanCeremony(state: StateManager): void {
515
519
  }
516
520
 
517
521
  // =============================================================================
518
- // DEDUP PHASE (synchronous, dry-runlogs candidates, no mutations)
522
+ // DEDUP PHASE (synchronous, logging only — candidates are queued for curation by dedup-phase.ts)
519
523
  // =============================================================================
520
524
 
521
525
  const DEDUP_DEFAULT_THRESHOLD = 0.85;
@@ -555,11 +559,10 @@ export function runDedupPhase(state: StateManager): void {
555
559
  const human = state.getHuman();
556
560
  const threshold = human.settings?.ceremony?.dedup_threshold ?? DEDUP_DEFAULT_THRESHOLD;
557
561
 
558
- console.log(`[ceremony:dedup] Running dry-run dedup (threshold: ${threshold})`);
562
+ console.log(`[ceremony:dedup] Scanning for dedup candidates (threshold: ${threshold})`);
559
563
 
560
564
  const types: Array<{ label: string; items: DedupableItem[] }> = [
561
565
  { label: "facts", items: human.facts },
562
- { label: "traits", items: human.traits },
563
566
  { label: "topics", items: human.topics },
564
567
  { label: "people", items: human.people },
565
568
  ];
@@ -618,11 +621,7 @@ export function queueRewritePhase(state: StateManager): void {
618
621
  itemsToScan.push({ item: fact, type: "fact" });
619
622
  }
620
623
  }
621
- for (const trait of human.traits) {
622
- if ((trait.description?.length ?? 0) > REWRITE_DESCRIPTION_THRESHOLD) {
623
- itemsToScan.push({ item: trait, type: "trait" });
624
- }
625
- }
624
+
626
625
  for (const topic of human.topics) {
627
626
  if ((topic.description?.length ?? 0) > REWRITE_DESCRIPTION_THRESHOLD) {
628
627
  itemsToScan.push({ item: topic, type: "topic" });
@@ -661,3 +660,18 @@ export function queueRewritePhase(state: StateManager): void {
661
660
 
662
661
  console.log(`[ceremony:rewrite] Queued ${itemsToScan.length} Phase 1 scan(s) at Low priority`);
663
662
  }
663
+
664
+ function queueEventSummaryForAll(state: StateManager, options?: ExtractionOptions): void {
665
+ const personas = state.persona_getAll();
666
+ const activePersonas = personas.filter(p =>
667
+ !p.is_paused &&
668
+ !p.is_archived &&
669
+ !p.is_static
670
+ );
671
+
672
+ let totalQueued = 0;
673
+ for (const persona of activePersonas) {
674
+ totalQueued += queueEventSummary(persona.id, state, options);
675
+ }
676
+ console.log(`[ceremony:event] Queued event summary scans for ${activePersonas.length} personas (${totalQueued} total chunks)`);
677
+ }
@@ -20,7 +20,7 @@ interface Cluster {
20
20
  // DEDUP CANDIDATE FINDING (copied from ceremony.ts)
21
21
  // =============================================================================
22
22
 
23
- const DEDUP_DEFAULT_THRESHOLD = 0.95;
23
+ const DEDUP_DEFAULT_THRESHOLD = 0.85; // Lowered from 0.95 based on experimental analysis: 0.95 only catches 3.9% of duplicate name groups, 0.85 catches 46.7%
24
24
 
25
25
  function findDedupCandidates<T extends DedupableItem>(
26
26
  items: T[],
@@ -127,13 +127,19 @@ function filterClusters(clusters: Cluster[]): Cluster[] {
127
127
 
128
128
  export function queueDedupPhase(state: StateManager): void {
129
129
  const human = state.getHuman();
130
+ const rewriteModel = human.settings?.rewrite_model;
131
+
132
+ if (!rewriteModel) {
133
+ console.log("[Dedup] rewrite_model not set — skipping dedup phase");
134
+ return;
135
+ }
136
+
130
137
  const threshold = human.settings?.ceremony?.dedup_threshold ?? DEDUP_DEFAULT_THRESHOLD;
131
138
 
132
139
  console.log(`[Dedup] Starting deduplication phase (threshold: ${threshold})`);
133
140
 
134
141
  const entityTypes: Array<{ type: DataItemType; items: DedupableItem[] }> = [
135
142
  { type: "fact", items: human.facts },
136
- { type: "trait", items: human.traits },
137
143
  { type: "topic", items: human.topics },
138
144
  { type: "person", items: human.people },
139
145
  ];
@@ -183,13 +189,13 @@ export function queueDedupPhase(state: StateManager): void {
183
189
  system: prompt.system,
184
190
  user: prompt.user,
185
191
  next_step: LLMNextStep.HandleDedupCurate,
192
+ model: rewriteModel,
186
193
  data: {
187
194
  entity_type: type,
188
- entity_ids: cluster.ids, // Lightweight stub (IDs only)
189
- ceremony_progress: 1 // Phase 1 (Dedup)
195
+ entity_ids: cluster.ids,
196
+ ceremony_progress: 1
190
197
  }
191
198
  });
192
-
193
199
  totalClusters++;
194
200
  }
195
201
  }