ei-tui 0.1.25 → 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 (78) 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 +10 -16
  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 +50 -39
  24. package/src/core/orchestrators/dedup-phase.ts +0 -1
  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 +99 -17
  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/rewrite.ts +3 -22
  45. package/src/prompts/ceremony/types.ts +3 -3
  46. package/src/prompts/generation/descriptions.ts +2 -2
  47. package/src/prompts/generation/types.ts +2 -2
  48. package/src/prompts/heartbeat/types.ts +2 -2
  49. package/src/prompts/human/event-scan.ts +122 -0
  50. package/src/prompts/human/fact-find.ts +106 -0
  51. package/src/prompts/human/fact-scan.ts +0 -2
  52. package/src/prompts/human/index.ts +17 -10
  53. package/src/prompts/human/person-match.ts +65 -0
  54. package/src/prompts/human/person-scan.ts +52 -59
  55. package/src/prompts/human/person-update.ts +241 -0
  56. package/src/prompts/human/topic-match.ts +65 -0
  57. package/src/prompts/human/topic-scan.ts +51 -71
  58. package/src/prompts/human/topic-update.ts +295 -0
  59. package/src/prompts/human/types.ts +63 -40
  60. package/src/prompts/index.ts +4 -8
  61. package/src/prompts/persona/topics-update.ts +2 -2
  62. package/src/prompts/persona/traits.ts +2 -2
  63. package/src/prompts/persona/types.ts +3 -3
  64. package/src/prompts/response/index.ts +1 -1
  65. package/src/prompts/response/sections.ts +9 -12
  66. package/src/prompts/response/types.ts +2 -3
  67. package/src/storage/embeddings.ts +1 -1
  68. package/src/storage/index.ts +1 -0
  69. package/src/storage/indexed.ts +174 -0
  70. package/src/storage/merge.ts +67 -2
  71. package/tui/src/commands/me.tsx +5 -14
  72. package/tui/src/commands/settings.tsx +15 -0
  73. package/tui/src/context/ei.tsx +5 -14
  74. package/tui/src/util/yaml-serializers.ts +48 -33
  75. package/src/cli/commands/traits.ts +0 -25
  76. package/src/prompts/human/item-match.ts +0 -74
  77. package/src/prompts/human/item-update.ts +0 -364
  78. package/src/prompts/human/trait-scan.ts +0 -115
@@ -1,12 +1,14 @@
1
1
  export { orchestratePersonaGeneration, type PartialPersona } from "./persona-generation.js";
2
2
  export {
3
- queueFactScan,
4
- queueTraitScan,
3
+ queueFactFind,
5
4
  queueTopicScan,
6
5
  queuePersonScan,
7
6
  queueAllScans,
8
- queueItemMatch,
9
- queueItemUpdate,
7
+ queueTopicMatch,
8
+ queueTopicUpdate,
9
+ queuePersonMatch,
10
+ queuePersonUpdate,
11
+ queueEventSummary,
10
12
  type ExtractionContext,
11
13
  type ExtractionOptions,
12
14
  } from "./human-extraction.js";
@@ -1,4 +1,4 @@
1
- import { LLMRequestType, LLMPriority, LLMNextStep, type Trait, type PersonaTopic } from "../types.js";
1
+ import { LLMRequestType, LLMPriority, LLMNextStep, type PersonaTrait, type PersonaTopic } from "../types.js";
2
2
  import type { StateManager } from "../state-manager.js";
3
3
  import { buildPersonaGenerationPrompt } from "../../prompts/index.js";
4
4
 
@@ -11,7 +11,7 @@ export interface PartialPersona {
11
11
  description?: string;
12
12
  short_description?: string;
13
13
  long_description?: string;
14
- traits?: Partial<Trait>[];
14
+ traits?: Partial<PersonaTrait>[];
15
15
  topics?: Partial<PersonaTopic>[];
16
16
  model?: string;
17
17
  group_primary?: string;
@@ -66,7 +66,7 @@ export function orchestratePersonaGeneration(
66
66
  stateManager.persona_update(partial.id, {
67
67
  short_description: partial.short_description,
68
68
  long_description: partial.long_description,
69
- traits: partial.traits as Trait[],
69
+ traits: partial.traits as PersonaTrait[],
70
70
  topics: partial.topics as PersonaTopic[],
71
71
  last_updated: now,
72
72
  });
@@ -9,7 +9,6 @@ import {
9
9
  type MessageQueryOptions,
10
10
  type HumanEntity,
11
11
  type Fact,
12
- type Trait,
13
12
  type Topic,
14
13
  type Person,
15
14
  type Quote,
@@ -33,6 +32,7 @@ import { registerReadMemoryExecutor, registerFileReadExecutor } from "./tools/in
33
32
  import { createReadMemoryExecutor } from "./tools/builtin/read-memory.js";
34
33
  import { EI_WELCOME_MESSAGE, EI_PERSONA_DEFINITION } from "../templates/welcome.js";
35
34
  import { shouldStartCeremony, startCeremony, handleCeremonyProgress } from "./orchestrators/index.js";
35
+ import { BUILT_IN_FACTS } from "./constants/built-in-facts.js";
36
36
 
37
37
  // Static module imports
38
38
  import {
@@ -70,7 +70,6 @@ import {
70
70
  getHuman,
71
71
  updateHuman,
72
72
  upsertFact,
73
- upsertTrait,
74
73
  upsertTopic,
75
74
  upsertPerson,
76
75
  removeDataItem,
@@ -191,10 +190,16 @@ export class Processor {
191
190
  }
192
191
  }
193
192
 
193
+ await this.completeInitialization();
194
+ }
195
+
196
+ private async completeInitialization(): Promise<void> {
194
197
  if (!this.stateManager.hasExistingData() || this.stateManager.persona_getAll().length === 0) {
195
198
  await this.bootstrapFirstRun();
196
199
  }
197
200
  this.bootstrapTools();
201
+ this.seedBuiltinFacts();
202
+ this.seedSettings();
198
203
  registerReadMemoryExecutor(createReadMemoryExecutor(this.searchHumanData.bind(this)));
199
204
  if (this.isTUI) {
200
205
  await registerFileReadExecutor();
@@ -571,6 +576,87 @@ export class Processor {
571
576
  }
572
577
  }
573
578
 
579
+ /**
580
+ * Seed 25 built-in facts if they don't exist yet.
581
+ * Called on every startup — safe to call repeatedly.
582
+ * New facts are created with empty descriptions and validated_date.
583
+ */
584
+ private seedBuiltinFacts(): void {
585
+ const human = this.stateManager.getHuman();
586
+ const existingFactNames = new Set(human.facts.map(f => f.name));
587
+
588
+ // BUILT_IN_FACTS imported at top of file
589
+ const now = new Date().toISOString();
590
+ let seededCount = 0;
591
+
592
+ for (const builtInFact of BUILT_IN_FACTS) {
593
+ if (existingFactNames.has(builtInFact.name)) continue;
594
+
595
+ const newFact: Fact = {
596
+ id: crypto.randomUUID(),
597
+ name: builtInFact.name,
598
+ description: '',
599
+ sentiment: 0,
600
+ validated_date: '',
601
+ last_updated: now,
602
+ };
603
+ human.facts.push(newFact);
604
+ seededCount++;
605
+ }
606
+
607
+ if (seededCount > 0) {
608
+ this.stateManager.setHuman(human);
609
+ console.log(`[Processor] Seeded ${seededCount} built-in facts`);
610
+ }
611
+ }
612
+
613
+ private seedSettings(): void {
614
+ const human = this.stateManager.getHuman();
615
+ let modified = false;
616
+
617
+ if (!human.settings) {
618
+ human.settings = {};
619
+ modified = true;
620
+ }
621
+
622
+ if (!human.settings.opencode) {
623
+ human.settings.opencode = {
624
+ integration: false,
625
+ polling_interval_ms: 1800000,
626
+ };
627
+ modified = true;
628
+ }
629
+
630
+ if (!human.settings.claudeCode) {
631
+ human.settings.claudeCode = {
632
+ integration: false,
633
+ polling_interval_ms: 1800000,
634
+ };
635
+ modified = true;
636
+ }
637
+
638
+ if (!human.settings.ceremony) {
639
+ human.settings.ceremony = {
640
+ time: "09:00",
641
+ };
642
+ modified = true;
643
+ }
644
+
645
+ if (!human.settings.backup) {
646
+ human.settings.backup = {
647
+ enabled: false,
648
+ max_backups: 24,
649
+ interval_ms: 3600000,
650
+ };
651
+ modified = true;
652
+ }
653
+
654
+ if (modified) {
655
+ this.stateManager.setHuman(human);
656
+ console.log(`[Processor] Seeded missing settings`);
657
+ }
658
+ }
659
+
574
660
  async stop(): Promise<void> {
575
661
  console.log(
576
662
  `[Processor ${this.instanceId}] stop() called, running=${this.running}, stopped=${this.stopped}`
@@ -676,13 +762,7 @@ export class Processor {
676
762
 
677
763
  this.pendingConflict = null;
678
764
  this.importAbortController = new AbortController();
679
- this.bootstrapTools();
680
- registerReadMemoryExecutor(createReadMemoryExecutor(this.searchHumanData.bind(this)));
681
- if (this.isTUI) {
682
- await registerFileReadExecutor();
683
- }
684
- this.running = true;
685
- this.runLoop();
765
+ await this.completeInitialization();
686
766
  this.interface.onStateImported?.();
687
767
  }
688
768
 
@@ -1093,7 +1173,10 @@ const toolNextSteps = new Set([
1093
1173
  }
1094
1174
  }
1095
1175
 
1096
- if (response.request.next_step === LLMNextStep.HandleHumanItemUpdate) {
1176
+ if (
1177
+ response.request.next_step === LLMNextStep.HandleTopicUpdate ||
1178
+ response.request.next_step === LLMNextStep.HandlePersonUpdate
1179
+ ) {
1097
1180
  this.interface.onHumanUpdated?.();
1098
1181
  this.interface.onQuoteAdded?.();
1099
1182
  }
@@ -1102,6 +1185,10 @@ const toolNextSteps = new Set([
1102
1185
  this.interface.onHumanUpdated?.();
1103
1186
  }
1104
1187
 
1188
+ if (response.request.next_step === LLMNextStep.HandleFactFind) {
1189
+ this.interface.onHumanUpdated?.();
1190
+ }
1191
+
1105
1192
  if (typeof response.request.data.ceremony_progress === "number") {
1106
1193
  handleCeremonyProgress(this.stateManager, response.request.data.ceremony_progress);
1107
1194
  }
@@ -1265,10 +1352,6 @@ const toolNextSteps = new Set([
1265
1352
  this.interface.onHumanUpdated?.();
1266
1353
  }
1267
1354
 
1268
- async upsertTrait(trait: Trait): Promise<void> {
1269
- await upsertTrait(this.stateManager, trait);
1270
- this.interface.onHumanUpdated?.();
1271
- }
1272
1355
 
1273
1356
  async upsertTopic(topic: Topic): Promise<void> {
1274
1357
  await upsertTopic(this.stateManager, topic);
@@ -1281,7 +1364,7 @@ const toolNextSteps = new Set([
1281
1364
  }
1282
1365
 
1283
1366
  async removeDataItem(
1284
- type: "fact" | "trait" | "topic" | "person",
1367
+ type: "fact" | "topic" | "person",
1285
1368
  id: string
1286
1369
  ): Promise<void> {
1287
1370
  await removeDataItem(this.stateManager, type, id);
@@ -1313,10 +1396,9 @@ const toolNextSteps = new Set([
1313
1396
 
1314
1397
  async searchHumanData(
1315
1398
  query: string,
1316
- options: { types?: Array<"fact" | "trait" | "topic" | "person" | "quote">; limit?: number } = {}
1399
+ options: { types?: Array<"fact" | "topic" | "person" | "quote">; limit?: number } = {}
1317
1400
  ): Promise<{
1318
1401
  facts: Fact[];
1319
- traits: Trait[];
1320
1402
  topics: Topic[];
1321
1403
  people: Person[];
1322
1404
  quotes: Quote[];
@@ -80,14 +80,13 @@ export async function filterHumanDataByVisibility(
80
80
  const DEFAULT_GROUP = "General";
81
81
 
82
82
  if (persona.id === "ei") {
83
- const [facts, traits, topics, people, quotes] = await Promise.all([
83
+ const [facts, topics, people, quotes] = await Promise.all([
84
84
  selectRelevantItems(human.facts, DATA_ITEM_LIMIT, currentMessage),
85
- selectRelevantItems(human.traits, DATA_ITEM_LIMIT, currentMessage),
86
85
  selectRelevantItems(human.topics, DATA_ITEM_LIMIT, currentMessage),
87
86
  selectRelevantItems(human.people, DATA_ITEM_LIMIT, currentMessage),
88
87
  selectRelevantQuotes(human.quotes ?? [], currentMessage),
89
88
  ]);
90
- return { facts, traits, topics, people, quotes };
89
+ return { facts, topics, people, quotes };
91
90
  }
92
91
 
93
92
  const visibleGroups = new Set<string>();
@@ -109,15 +108,14 @@ export async function filterHumanDataByVisibility(
109
108
  return effectiveGroups.some((g) => visibleGroups.has(g));
110
109
  });
111
110
 
112
- const [facts, traits, topics, people, quotes] = await Promise.all([
111
+ const [facts, topics, people, quotes] = await Promise.all([
113
112
  selectRelevantItems(filterByGroup(human.facts), DATA_ITEM_LIMIT, currentMessage),
114
- selectRelevantItems(filterByGroup(human.traits), DATA_ITEM_LIMIT, currentMessage),
115
113
  selectRelevantItems(filterByGroup(human.topics), DATA_ITEM_LIMIT, currentMessage),
116
114
  selectRelevantItems(filterByGroup(human.people), DATA_ITEM_LIMIT, currentMessage),
117
115
  selectRelevantQuotes(groupFilteredQuotes, currentMessage),
118
116
  ]);
119
117
 
120
- return { facts, traits, topics, people, quotes };
118
+ return { facts, topics, people, quotes };
121
119
  }
122
120
 
123
121
  // =============================================================================
@@ -1,10 +1,9 @@
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
 
3
3
  export function createDefaultHumanEntity(): HumanEntity {
4
4
  return {
5
5
  entity: "human",
6
6
  facts: [],
7
- traits: [],
8
7
  topics: [],
9
8
  people: [],
10
9
  quotes: [],
@@ -63,30 +62,6 @@ export class HumanState {
63
62
  return false;
64
63
  }
65
64
 
66
- trait_upsert(trait: Trait): void {
67
- const idx = this.human.traits.findIndex((t) => t.id === trait.id);
68
- trait.last_updated = new Date().toISOString();
69
- if (idx >= 0) {
70
- this.human.traits[idx] = trait;
71
- } else {
72
- this.human.traits.push(trait);
73
- }
74
- this.human.last_updated = new Date().toISOString();
75
- }
76
-
77
- trait_remove(id: string): boolean {
78
- const idx = this.human.traits.findIndex((t) => t.id === id);
79
- if (idx >= 0) {
80
- this.human.traits.splice(idx, 1);
81
- // Clean up quote references
82
- this.human.quotes.forEach((q) => {
83
- q.data_item_ids = q.data_item_ids.filter((itemId) => itemId !== id);
84
- });
85
- this.human.last_updated = new Date().toISOString();
86
- return true;
87
- }
88
- return false;
89
- }
90
65
 
91
66
  topic_upsert(topic: Topic): void {
92
67
  const idx = this.human.topics.findIndex((t) => t.id === topic.id);
@@ -213,7 +213,7 @@ export class PersonaState {
213
213
  return removed;
214
214
  }
215
215
 
216
- messages_getUnextracted(personaId: string, flag: "f" | "r" | "p" | "o", limit?: number): Message[] {
216
+ messages_getUnextracted(personaId: string, flag: "f" | "t" | "p" | "e", limit?: number): Message[] {
217
217
  const data = this.personas.get(personaId);
218
218
  if (!data) return [];
219
219
  const unextracted = data.messages.filter(m => m[flag] !== true);
@@ -223,7 +223,7 @@ export class PersonaState {
223
223
  return unextracted.map(m => ({ ...m }));
224
224
  }
225
225
 
226
- messages_markExtracted(personaId: string, messageIds: string[], flag: "f" | "r" | "p" | "o"): number {
226
+ messages_markExtracted(personaId: string, messageIds: string[], flag: "f" | "t" | "p" | "e"): number {
227
227
  const data = this.personas.get(personaId);
228
228
  if (!data) return 0;
229
229
  const idsSet = new Set(messageIds);
@@ -3,7 +3,6 @@ import type {
3
3
  PersonaEntity,
4
4
  Message,
5
5
  Fact,
6
- Trait,
7
6
  Topic,
8
7
  Person,
9
8
  Quote,
@@ -14,6 +13,7 @@ import type {
14
13
  ToolDefinition,
15
14
  ToolProvider,
16
15
  } from "./types.js";
16
+ import { BUILT_IN_FACT_NAMES } from './constants/built-in-facts.js';
17
17
  import type { Storage } from "../storage/interface.js";
18
18
  import {
19
19
  HumanState,
@@ -43,6 +43,8 @@ export class StateManager {
43
43
  this.tools = state.tools ?? [];
44
44
  this.providers = state.providers ?? [];
45
45
  this.migrateLearnedByToIds();
46
+ this.migrateFactValidation();
47
+ this.migrateMessageFlags();
46
48
  } else {
47
49
  this.humanState.load(createDefaultHumanEntity());
48
50
  }
@@ -80,13 +82,114 @@ export class StateManager {
80
82
  dirty = true;
81
83
  }
82
84
  };
83
- [...human.facts, ...human.traits, ...human.topics, ...human.people].forEach(migrateItem);
85
+ [...human.facts, ...human.topics, ...human.people].forEach(migrateItem);
84
86
  if (dirty) {
85
87
  this.humanState.set(human);
86
88
  console.log("[StateManager] Migrated learned_by fields from display names to persona IDs");
87
89
  }
88
90
  }
89
91
 
92
+ /**
93
+ * Migration: Facts used to have a 'validated' field (now removed).
94
+ * Now, only 25 built-in facts remain; others are converted to Topics with category='Fact'.
95
+ * - Facts with 'validated' field whose name is NOT in BUILT_IN_FACT_NAMES → move to Topics
96
+ * - Facts with 'validated' field whose name IS in BUILT_IN_FACT_NAMES → strip 'validated'
97
+ * No-op for already-migrated data (no 'validated' field present).
98
+ */
99
+ private migrateFactValidation(): void {
100
+ const human = this.humanState.get();
101
+
102
+ // Check if any fact has 'validated' property (old format detection)
103
+ const hasOldFormat = human.facts.some((f) => 'validated' in f);
104
+ if (!hasOldFormat) return;
105
+
106
+ let dirty = false;
107
+ const newFacts: Fact[] = [];
108
+ let movedCount = 0;
109
+ let strippedCount = 0;
110
+ // Define legacy fact interface for type-safe migration
111
+ interface LegacyFact extends Fact {
112
+ validated?: boolean;
113
+ }
114
+
115
+ for (const fact of human.facts) {
116
+ if (!('validated' in fact)) {
117
+ // Already migrated fact, keep as-is
118
+ newFacts.push(fact);
119
+ continue;
120
+ }
121
+
122
+ if (BUILT_IN_FACT_NAMES.has(fact.name)) {
123
+ // Matching built-in: strip 'validated' field, preserve description
124
+ const { validated, ...cleanedFact } = fact as LegacyFact;
125
+ newFacts.push(cleanedFact);
126
+ strippedCount++;
127
+ dirty = true;
128
+ } else {
129
+ // Non-matching: move to Topics
130
+ const newTopic: Topic = {
131
+ id: crypto.randomUUID(),
132
+ name: fact.name,
133
+ description: fact.description,
134
+ category: 'Fact',
135
+ sentiment: fact.sentiment,
136
+ exposure_current: 0.3,
137
+ exposure_desired: 0.3,
138
+ last_updated: fact.last_updated,
139
+ learned_by: fact.learned_by,
140
+ last_changed_by: fact.last_changed_by,
141
+ persona_groups: fact.persona_groups,
142
+ embedding: fact.embedding,
143
+ };
144
+ human.topics.push(newTopic);
145
+ movedCount++;
146
+ dirty = true;
147
+ }
148
+ }
149
+
150
+ if (dirty) {
151
+ human.facts = newFacts;
152
+ this.humanState.set(human);
153
+ console.log(
154
+ `[StateManager] Migrated fact validation: moved ${movedCount} non-matching facts to Topics, stripped 'validated' from ${strippedCount} built-in facts`
155
+ );
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Migration: Message extraction flags were incorrectly named.
161
+ * Old: p=Topics, o=People, r=Traits (dead)
162
+ * New: t=Topics, p=People (r and o removed)
163
+ * Detects old format by presence of 'o' flag on any message.
164
+ */
165
+ private migrateMessageFlags(): void {
166
+ const personas = this.personaState.getAll();
167
+ let migratedCount = 0;
168
+
169
+ for (const persona of personas) {
170
+ // Access raw message objects to detect and remap old flags
171
+ const rawMessages = (this.personaState as unknown as { personas: Map<string, { messages: Array<Record<string, unknown>> }> }).personas.get(persona.id)?.messages ?? [];
172
+ const hasOldFormat = rawMessages.some(m => 'o' in m || 'r' in m);
173
+ if (!hasOldFormat) continue;
174
+
175
+ for (const msg of rawMessages) {
176
+ // Remap: old p (topics) → new t; old o (people) → new p
177
+ const oldP = msg['p']; // was topics
178
+ const oldO = msg['o']; // was people
179
+ msg['t'] = oldP; // topics: old p → new t
180
+ msg['p'] = oldO; // people: old o → new p
181
+ delete msg['r']; // trait flag dead
182
+ delete msg['o']; // old people flag dead
183
+ migratedCount++;
184
+ }
185
+ }
186
+
187
+ if (migratedCount > 0) {
188
+ this.scheduleSave();
189
+ console.log(`[StateManager] Migrated message flags (p→t, o→p, removed r/o) for ${migratedCount} messages`);
190
+ }
191
+ }
192
+
90
193
  /**
91
194
  * Returns true if value looks like a persona ID (UUID or the special "ei" id).
92
195
  * Display names are free-form strings that won't match UUID format.
@@ -132,16 +235,6 @@ export class StateManager {
132
235
  return result;
133
236
  }
134
237
 
135
- human_trait_upsert(trait: Trait): void {
136
- this.humanState.trait_upsert(trait);
137
- this.scheduleSave();
138
- }
139
-
140
- human_trait_remove(id: string): boolean {
141
- const result = this.humanState.trait_remove(id);
142
- this.scheduleSave();
143
- return result;
144
- }
145
238
 
146
239
  human_topic_upsert(topic: Topic): void {
147
240
  this.humanState.topic_upsert(topic);
@@ -303,11 +396,11 @@ export class StateManager {
303
396
  return result;
304
397
  }
305
398
 
306
- messages_getUnextracted(personaId: string, flag: "f" | "r" | "p" | "o", limit?: number): Message[] {
399
+ messages_getUnextracted(personaId: string, flag: "f" | "t" | "p" | "e", limit?: number): Message[] {
307
400
  return this.personaState.messages_getUnextracted(personaId, flag, limit);
308
401
  }
309
402
 
310
- messages_markExtracted(personaId: string, messageIds: string[], flag: "f" | "r" | "p" | "o"): number {
403
+ messages_markExtracted(personaId: string, messageIds: string[], flag: "f" | "t" | "p" | "e"): number {
311
404
  const result = this.personaState.messages_markExtracted(personaId, messageIds, flag);
312
405
  this.scheduleSave();
313
406
  return result;
@@ -5,12 +5,12 @@
5
5
  * The searchHumanData function is injected at construction to avoid circular deps.
6
6
  */
7
7
  import type { ToolExecutor } from "../types.js";
8
- import type { Fact, Trait, Topic, Person, Quote } from "../../types.js";
8
+ import type { Fact, Topic, Person, Quote } from "../../types.js";
9
9
 
10
10
  type SearchHumanData = (
11
11
  query: string,
12
- options?: { types?: Array<"fact" | "trait" | "topic" | "person" | "quote">; limit?: number }
13
- ) => Promise<{ facts: Fact[]; traits: Trait[]; topics: Topic[]; people: Person[]; quotes: Quote[] }>;
12
+ options?: { types?: Array<"fact" | "topic" | "person" | "quote">; limit?: number }
13
+ ) => Promise<{ facts: Fact[]; topics: Topic[]; people: Person[]; quotes: Quote[] }>;
14
14
 
15
15
  export function createReadMemoryExecutor(searchHumanData: SearchHumanData): ToolExecutor {
16
16
  return {
@@ -26,20 +26,19 @@ export function createReadMemoryExecutor(searchHumanData: SearchHumanData): Tool
26
26
 
27
27
  const types = Array.isArray(args.types)
28
28
  ? (args.types.filter(
29
- t => typeof t === "string" && ["fact", "trait", "topic", "person", "quote"].includes(t)
30
- ) as Array<"fact" | "trait" | "topic" | "person" | "quote">)
29
+ t => typeof t === "string" && ["fact", "topic", "person", "quote"].includes(t)
30
+ ) as Array<"fact" | "topic" | "person" | "quote">)
31
31
  : undefined;
32
32
 
33
33
  const limit = typeof args.limit === "number" && args.limit > 0 ? Math.min(args.limit, 20) : 10;
34
34
 
35
35
  const results = await searchHumanData(query, { types, limit });
36
36
 
37
- const total = results.facts.length + results.traits.length + results.topics.length + results.people.length + results.quotes.length;
38
- console.log(`[read_memory] query="${query}" => ${total} results (facts=${results.facts.length}, traits=${results.traits.length}, topics=${results.topics.length}, people=${results.people.length}, quotes=${results.quotes.length})`);
37
+ const total = results.facts.length + results.topics.length + results.people.length + results.quotes.length;
38
+ console.log(`[read_memory] query="${query}" => ${total} results (facts=${results.facts.length}, topics=${results.topics.length}, people=${results.people.length}, quotes=${results.quotes.length})`);
39
39
 
40
40
  const output: Record<string, unknown[]> = {};
41
41
  if (results.facts.length > 0) output.facts = results.facts.map(f => ({ name: f.name, description: f.description }));
42
- if (results.traits.length > 0) output.traits = results.traits.map(t => ({ name: t.name, description: t.description }));
43
42
  if (results.topics.length > 0) output.topics = results.topics.map(t => ({ name: t.name, description: t.description }));
44
43
  if (results.people.length > 0) output.people = results.people.map(p => ({ name: p.name, relationship: p.relationship, description: p.description }));
45
44
  if (results.quotes.length > 0) output.quotes = results.quotes.map(q => ({ text: q.text, speaker: q.speaker }));
@@ -3,7 +3,6 @@
3
3
  * Source of truth: CONTRACTS.md
4
4
  */
5
5
 
6
- import type { ValidationLevel } from "./enums.js";
7
6
 
8
7
  export interface DataItemBase {
9
8
  id: string;
@@ -18,11 +17,10 @@ export interface DataItemBase {
18
17
  }
19
18
 
20
19
  export interface Fact extends DataItemBase {
21
- validated: ValidationLevel;
22
20
  validated_date: string;
23
21
  }
24
22
 
25
- export interface Trait extends DataItemBase {
23
+ export interface PersonaTrait extends DataItemBase {
26
24
  strength?: number;
27
25
  }
28
26
 
@@ -77,4 +75,4 @@ export interface Quote {
77
75
 
78
76
  export type DataItemType = "fact" | "trait" | "topic" | "person";
79
77
 
80
- export type DataItem = Fact | Trait | Topic | Person;
78
+ export type DataItem = Fact | PersonaTrait | Topic | Person;
@@ -3,7 +3,7 @@
3
3
  * Source of truth: CONTRACTS.md
4
4
  */
5
5
 
6
- import type { Fact, Trait, Topic, Person, Quote, PersonaTopic } from "./data-items.js";
6
+ import type { Fact, PersonaTrait, Topic, Person, Quote, PersonaTopic } from "./data-items.js";
7
7
  import type { ProviderType } from "./enums.js";
8
8
 
9
9
  export interface SyncCredentials {
@@ -14,6 +14,8 @@ export interface SyncCredentials {
14
14
  export interface OpenCodeSettings {
15
15
  integration?: boolean;
16
16
  polling_interval_ms?: number; // Default: 1800000 (30 min)
17
+ extraction_model?: string; // "Provider:model" for extraction. Unset = uses default_model.
18
+ extraction_token_limit?: number; // Token budget for extraction chunking. Unset = resolved from model.
17
19
  last_sync?: string; // ISO timestamp
18
20
  extraction_point?: string; // ISO timestamp - cursor for single-session archive scan
19
21
  processed_sessions?: Record<string, string>; // sessionId → ISO timestamp of last import
@@ -25,6 +27,7 @@ export interface CeremonyConfig {
25
27
  decay_rate?: number; // Default: 0.1
26
28
  explore_threshold?: number; // Default: 3
27
29
  dedup_threshold?: number; // Cosine similarity threshold for dedup candidates. Default: 0.85
30
+ event_window_hours?: number; // Gap threshold for conversation window detection. Default: 8
28
31
  }
29
32
 
30
33
  export interface BackupConfig {
@@ -88,7 +91,6 @@ export interface HumanSettings {
88
91
  export interface HumanEntity {
89
92
  entity: "human";
90
93
  facts: Fact[];
91
- traits: Trait[];
92
94
  topics: Topic[];
93
95
  people: Person[];
94
96
  quotes: Quote[];
@@ -107,7 +109,7 @@ export interface PersonaEntity {
107
109
  model?: string;
108
110
  group_primary?: string | null;
109
111
  groups_visible?: string[];
110
- traits: Trait[];
112
+ traits: PersonaTrait[];
111
113
  topics: PersonaTopic[];
112
114
  is_paused: boolean;
113
115
  pause_until?: string;
@@ -129,7 +131,7 @@ export interface PersonaCreationInput {
129
131
  aliases?: string[];
130
132
  long_description?: string;
131
133
  short_description?: string;
132
- traits?: Partial<Trait>[];
134
+ traits?: Partial<PersonaTrait>[];
133
135
  topics?: Partial<PersonaTopic>[];
134
136
  model?: string;
135
137
  group_primary?: string;
@@ -9,11 +9,6 @@ export enum ContextStatus {
9
9
  Never = "never",
10
10
  }
11
11
 
12
- export enum ValidationLevel {
13
- None = "none", // Fresh data, never acknowledged
14
- Ei = "ei", // Ei mentioned it to user (don't mention again)
15
- Human = "human", // User explicitly confirmed (locked)
16
- }
17
12
  export enum LLMRequestType {
18
13
  Response = "response",
19
14
  JSON = "json",
@@ -30,12 +25,13 @@ export enum LLMNextStep {
30
25
  HandlePersonaResponse = "handlePersonaResponse",
31
26
  HandlePersonaGeneration = "handlePersonaGeneration",
32
27
  HandlePersonaDescriptions = "handlePersonaDescriptions",
33
- HandleHumanFactScan = "handleHumanFactScan",
34
- HandleHumanTraitScan = "handleHumanTraitScan",
28
+ HandleFactFind = "handleFactFind",
35
29
  HandleHumanTopicScan = "handleHumanTopicScan",
36
30
  HandleHumanPersonScan = "handleHumanPersonScan",
37
- HandleHumanItemMatch = "handleHumanItemMatch",
38
- HandleHumanItemUpdate = "handleHumanItemUpdate",
31
+ HandleTopicMatch = "handleTopicMatch",
32
+ HandleTopicUpdate = "handleTopicUpdate",
33
+ HandlePersonMatch = "handlePersonMatch",
34
+ HandlePersonUpdate = "handlePersonUpdate",
39
35
  HandlePersonaTraitExtraction = "handlePersonaTraitExtraction",
40
36
  HandlePersonaTopicScan = "handlePersonaTopicScan",
41
37
  HandlePersonaTopicMatch = "handlePersonaTopicMatch",
@@ -54,6 +50,7 @@ export enum LLMNextStep {
54
50
  HandleRewriteScan = "handleRewriteScan",
55
51
  HandleRewriteRewrite = "handleRewriteRewrite",
56
52
  HandleDedupCurate = "handleDedupCurate",
53
+ HandleEventScan = "handleEventScan",
57
54
  }
58
55
 
59
56
  export enum ProviderType {
@@ -18,9 +18,9 @@ export interface Message {
18
18
  // Extraction completion flags (omit when false to save space)
19
19
  // Single-letter names minimize storage overhead for large message histories
20
20
  f?: boolean; // Fact extraction completed
21
- r?: boolean; // tRait extraction completed
21
+ t?: boolean; // Topic extraction completed
22
22
  p?: boolean; // Person extraction completed
23
- o?: boolean; // tOpic extraction completed
23
+ e?: boolean; // Event (epic) extraction completed
24
24
  // Image generation fields (web-only, ephemeral)
25
25
  _synthesis?: boolean; // True if message was created by multi-message synthesis
26
26