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
package/README.md CHANGED
@@ -116,6 +116,48 @@ Opencode saves all of its sessions locally, either in a JSON structure or, if yo
116
116
 
117
117
  Then, Opencode can call into Ei and pull those details back out. That's why you always have a side-project or two going. See [TUI Readme](tui/README.md)
118
118
 
119
+ ## Built-in Tool Integrations
120
+
121
+ Personas can use tools. Not just read-from-memory tools — *actual* tools. Web search. Your music. Your filesystem. Here's what ships with Ei out of the box:
122
+
123
+ ### Ei Built-ins (always available, no setup)
124
+
125
+ | Tool | What it does |
126
+ |------|-------------|
127
+ | `read_memory` | Semantic search of your personal memory — facts, traits, topics, people, quotes. Personas call this automatically when the conversation touches something they might know about you. |
128
+ | `file_read` | Read a file from your local filesystem *(TUI only)* |
129
+ | `list_directory` | Explore folder structure *(TUI only)* |
130
+ | `directory_tree` | Recursive directory tree *(TUI only)* |
131
+ | `search_files` | Find files by name pattern *(TUI only)* |
132
+ | `grep` | Search file contents by regex *(TUI only)* |
133
+ | `get_file_info` | File/directory metadata *(TUI only)* |
134
+
135
+ The filesystem tools make Ei a legitimate coding assistant in the TUI. Ask a persona to review a file, understand a project structure, or track down where something is defined — it can actually look.
136
+
137
+ ### Tavily Web Search (requires free API key)
138
+
139
+ | Tool | What it does |
140
+ |------|-------------|
141
+ | `tavily_web_search` | Real-time web search — current events, fact-checking, anything that needs up-to-date information |
142
+ | `tavily_news_search` | Recent news articles |
143
+
144
+ Get a free key at [tavily.com](https://tavily.com) (1,000 requests/month free tier). Add it in **Settings → Tool Kits → Tavily Search**.
145
+
146
+ ### Spotify (requires OAuth connection)
147
+
148
+ | Tool | What it does |
149
+ |------|-------------|
150
+ | `get_currently_playing` | What's playing right now — artist, title, album, progress |
151
+ | `get_liked_songs` | Your full liked songs library |
152
+
153
+ Connect in **Settings → Tool Kits → Spotify**. Once connected, personas can ask what you're listening to and actually know. Music-aware conversations.
154
+
155
+ ### Assigning Tools to Personas
156
+
157
+ Tools aren't global — you choose which personas get access. Edit a persona and toggle the tools it can use. A focused work persona might only have filesystem tools. A general-purpose companion might have everything.
158
+
159
+ ---
160
+
119
161
  ## Technical Details
120
162
 
121
163
  This project is separated into five (5) logical parts:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ei-tui",
3
- "version": "0.1.24",
3
+ "version": "0.2.0",
4
4
  "author": "Flare576",
5
5
  "repository": {
6
6
  "type": "git",
package/src/README.md CHANGED
@@ -10,7 +10,7 @@ There are two distinct types of data: Human and Persona.
10
10
 
11
11
  ## Human
12
12
 
13
- Human data is sort of the "Global" data - Each Persona can read and write elements to the humans Facts, Traits, People, and Topics. In addition, there are "Quotes" that can tie to those four types of data.
13
+ Human data is sort of the "Global" data - Each Persona can read and write elements to the humans Facts, People, and Topics. In addition, there are "Quotes" that can tie to those three types of data.
14
14
 
15
15
  As the user uses the system, it tries to keep track of several data points for these elements:
16
16
 
@@ -24,12 +24,9 @@ As the user uses the system, it tries to keep track of several data points for t
24
24
  * Current: How much the user has talked or heard about a subject, where:
25
25
  + 0.0: Obi-Wan Kenobi ...now that's a name I've not heard in a long time
26
26
  + 1.0: The user just spent 4 hours talking about Star Wars
27
- - Strength: The system will try to gauge how strongly you exhibit a Trait
28
- * 1.0 on "Visual Learner" would mean that you've said or shown that it is the absolute best way for you to learn
29
- * 0.0 on "Public Speaker" would mean you've said or shown that you have no desire, aptitude, or willingness to present
30
- - Validated: "Facts" have proven almost as hard to get right as Traits, so I added a way for Ei and you to mark the ones that are true as "Validated"
27
+ - Validated: "Facts" have proven tricky, so I added a way for Ei and you to mark the ones that are true as "Validated"
31
28
 
32
- Each of those types represents a piece of what the system "knows" about the person, and all but "Traits" are kept up-to-date as the person chats with Personas, but not on always on every message. On each message to a Persona, a check is made:
29
+ Each of those types represents a piece of what the system "knows" about the person, and they're kept up-to-date as the person chats with Personas, but not always on every message. On each message to a Persona, a check is made:
33
30
 
34
31
  ```
35
32
  if(Person.newMessages > count_of_human_[type]) {
@@ -37,9 +34,7 @@ if(Person.newMessages > count_of_human_[type]) {
37
34
  }
38
35
  ```
39
36
 
40
- Again, except for Traits<sup>1</sup>, this is to extract quotes, description updates, title updates, etc. for the conversations the user is having, and keep them feeling alive.
41
-
42
- > <sup>1</sup> Traits are unique because, after trying to extract them in the same way as the other pieces of data, I realized that it's sorta hard to understand a core aspect of someone in one message, or even 10. Even doing this analysis over a full 24 hours hasn't proven to be particularly effective, but it's the best we have so far.
37
+ This extracts quotes, description updates, title updates, etc. for the conversations the user is having, and keeps them feeling alive.
43
38
 
44
39
  ## Persona
45
40
 
@@ -72,8 +67,6 @@ I also frequently refer to this as "Extract," but this is the first step where w
72
67
 
73
68
  Since we also pull out details during normal discourse (see above), this is the less-important step at this point, but still vital for catching up with the last few messages, or Personas that only received a few messages during the day and may not have hit the current limit for natural extraction.
74
69
 
75
- Additionally, this is the ONLY time when Human Traits are created or updated - after (hopefully) enough messages have been exchanged for an agent to analyze it and say "Yup, Flare is _definitely_ verbose."
76
-
77
70
  ### Exposure Adjustment
78
71
 
79
72
  Exposure is calculated by two metrics - `desired` and `current`. If an entity REALLY likes talking about a subject, their `desired` will be very high (1.0 max), ranging down to 0.0 for subjects which that entity does NOT wish to discuss. You may have guessed already, but `current` is how much they've recently talked about a topic.
package/src/cli/README.md CHANGED
@@ -6,7 +6,6 @@ ei # Start the TUI
6
6
  ei "query string" # Return up to 10 results across all types
7
7
  ei -n 5 "query string" # Return up to 5 results
8
8
  ei facts -n 5 "query string" # Return up to 5 facts
9
- ei traits -n 5 "query string" # Return up to 5 traits
10
9
  ei people -n 5 "query string" # Return up to 5 people
11
10
  ei topics -n 5 "query string" # Return up to 5 topics
12
11
  ei quotes -n 5 "query string" # Return up to 5 quotes
@@ -15,7 +14,7 @@ echo <id> | ei --id # Look up entity by ID from stdin
15
14
  ei --install # Install the Ei tool for OpenCode
16
15
  ```
17
16
 
18
- Type aliases: `fact`, `trait`, `person`, `topic`, `quote` all work (singular or plural).
17
+ Type aliases: `fact`, `person`, `topic`, `quote` all work (singular or plural).
19
18
 
20
19
  # An Agentic Tool
21
20
 
@@ -37,12 +36,12 @@ This writes `~/.config/opencode/tools/ei.ts` with a complete tool definition. Re
37
36
 
38
37
  ## What the Tool Provides
39
38
 
40
- The installed tool gives OpenCode agents access to all five data types with proper Zod-validated args:
39
+ The installed tool gives OpenCode agents access to all four data types with proper Zod-validated args:
41
40
 
42
41
  | Arg | Type | Description |
43
42
  |-----|------|-------------|
44
43
  | `query` | string (required) | Search text, or entity ID when `lookup=true` |
45
- | `type` | enum (optional) | `facts` \| `traits` \| `people` \| `topics` \| `quotes` — omit for balanced results |
44
+ | `type` | enum (optional) | `facts` \| `people` \| `topics` \| `quotes` — omit for balanced results |
46
45
  | `limit` | number (optional) | Max results, default 10 |
47
46
  | `lookup` | boolean (optional) | If true, fetch single entity by ID |
48
47
 
@@ -50,7 +49,7 @@ The installed tool gives OpenCode agents access to all five data types with prop
50
49
 
51
50
  All search commands return arrays. Each result includes a `type` field.
52
51
 
53
- **Fact / Trait / Person / Topic**: `{ type, id, name, description, sentiment, ...type-specific fields }`
52
+ **Fact / Person / Topic**: `{ type, id, name, description, sentiment, ...type-specific fields }`
54
53
 
55
54
  **Quote**: `{ type, id, text, speaker, timestamp, linked_items[] }`
56
55
 
@@ -1,4 +1,4 @@
1
- import type { StorageState, Quote, Fact, Trait, Person, Topic } from "../core/types";
1
+ import type { StorageState, Quote, Fact, Person, Topic } from "../core/types";
2
2
  import { decodeAllEmbeddings } from "../storage/embeddings";
3
3
  import { crossFind } from "../core/utils/index.ts";
4
4
  import { join } from "path";
@@ -67,15 +67,6 @@ export interface FactResult {
67
67
  name: string;
68
68
  description: string;
69
69
  sentiment: number;
70
- validated: string;
71
- }
72
-
73
- export interface TraitResult {
74
- id: string;
75
- name: string;
76
- description: string;
77
- strength: number;
78
- sentiment: number;
79
70
  }
80
71
 
81
72
  export interface PersonResult {
@@ -97,17 +88,16 @@ export interface TopicResult {
97
88
  export type BalancedResult =
98
89
  | ({ type: "quote" } & QuoteResult)
99
90
  | ({ type: "fact" } & FactResult)
100
- | ({ type: "trait" } & TraitResult)
101
91
  | ({ type: "person" } & PersonResult)
102
92
  | ({ type: "topic" } & TopicResult);
103
93
 
104
- const DATA_TYPES = ["quote", "fact", "trait", "person", "topic"] as const;
94
+ const DATA_TYPES = ["quote", "fact", "person", "topic"] as const;
105
95
  type DataType = typeof DATA_TYPES[number];
106
96
 
107
97
  interface ScoredEntry {
108
98
  type: DataType;
109
99
  similarity: number;
110
- mapped: QuoteResult | FactResult | TraitResult | PersonResult | TopicResult;
100
+ mapped: QuoteResult | FactResult | PersonResult | TopicResult;
111
101
  itemId: string;
112
102
  }
113
103
 
@@ -117,7 +107,6 @@ export function resolveLinkedItems(dataItemIds: string[], state: StorageState):
117
107
  { type: "topic", source: state.human.topics },
118
108
  { type: "person", source: state.human.people },
119
109
  { type: "fact", source: state.human.facts },
120
- { type: "trait", source: state.human.traits },
121
110
  ];
122
111
  for (const { type, source } of collections) {
123
112
  for (const entity of source) {
@@ -144,19 +133,9 @@ function mapFact(fact: Fact): FactResult {
144
133
  name: fact.name,
145
134
  description: fact.description,
146
135
  sentiment: fact.sentiment,
147
- validated: fact.validated,
148
136
  };
149
137
  }
150
138
 
151
- function mapTrait(trait: Trait): TraitResult {
152
- return {
153
- id: trait.id,
154
- name: trait.name,
155
- description: trait.description,
156
- strength: trait.strength ?? 0.5,
157
- sentiment: trait.sentiment,
158
- };
159
- }
160
139
 
161
140
  function mapPerson(person: Person): PersonResult {
162
141
  return {
@@ -200,7 +179,6 @@ export async function retrieveBalanced(
200
179
  }> = [
201
180
  { type: "quote", items: state.human.quotes, mapper: (q: Quote) => mapQuote(q, state) },
202
181
  { type: "fact", items: state.human.facts, mapper: mapFact },
203
- { type: "trait", items: state.human.traits, mapper: mapTrait },
204
182
  { type: "person", items: state.human.people, mapper: mapPerson },
205
183
  { type: "topic", items: state.human.topics, mapper: mapTopic },
206
184
  ];
package/src/cli.ts CHANGED
@@ -20,8 +20,6 @@ const TYPE_ALIASES: Record<string, string> = {
20
20
  quotes: "quotes",
21
21
  fact: "facts",
22
22
  facts: "facts",
23
- trait: "traits",
24
- traits: "traits",
25
23
  person: "people",
26
24
  people: "people",
27
25
  topic: "topics",
@@ -44,7 +42,6 @@ Usage:
44
42
  Types:
45
43
  quote / quotes Quotes from conversation history
46
44
  fact / facts Facts about the user
47
- trait / traits Personality traits
48
45
  person / people People from the user's life
49
46
  topic / topics Topics of interest
50
47
 
@@ -58,7 +55,6 @@ Examples:
58
55
  ei "debugging" # Search everything
59
56
  ei -n 5 "API design" # Top 5 across all types
60
57
  ei quote "you guessed it" # Search quotes only
61
- ei trait -n 3 "problem solving" # Top 3 matching traits
62
58
  ei --id abc-123 # Look up entity by ID
63
59
  ei "memory leak" | jq .[0].id | ei --id # Pipe ID from search
64
60
  `);
@@ -71,7 +67,7 @@ function buildOpenCodeToolContent(): string {
71
67
  'export default tool({',
72
68
  ' description: [',
73
69
  ' "Search the user\'s Ei knowledge base \u2014 a persistent memory store built from conversations.",',
74
- ' "Returns facts, personality traits, people, topics of interest, and quotes.",',
70
+ ' "Returns facts, people, topics of interest, and quotes.",',
75
71
  ' "Use this to recall anything about the user: preferences, relationships, or past discussions.",',
76
72
  ' "Results include entity IDs that can be passed back with lookup=true to get full detail.",',
77
73
  ' ].join(" "),',
@@ -80,10 +76,10 @@ function buildOpenCodeToolContent(): string {
80
76
  ' "Search text, or an entity ID when lookup=true. Supports natural language."',
81
77
  ' ),',
82
78
  ' type: tool.schema',
83
- ' .enum(["facts", "traits", "people", "topics", "quotes"])',
79
+ ' .enum(["facts", "people", "topics", "quotes"])',
84
80
  ' .optional()',
85
81
  ' .describe(',
86
- ' "Filter to a specific data type. Omit to search all types (balanced across all 5)."',
82
+ ' "Filter to a specific data type. Omit to search all types (balanced across all 4).",',
87
83
  ' ),',
88
84
  ' limit: tool.schema',
89
85
  ' .number()',
@@ -10,7 +10,7 @@ core/
10
10
  ├── state-manager.ts # In-memory state + persistence
11
11
  ├── queue-processor.ts # LLM request queue with priorities
12
12
  ├── llm-client.ts # Multi-provider LLM abstraction
13
- ├── types.ts # All core types (source: CONTRACTS.md)
13
+ ├── types.ts # All core types (canonical source CONTRACTS.md defers to these)
14
14
  ├── handlers/ # LLM response handlers
15
15
  ├── orchestrators/ # Multi-step workflows
16
16
  ├── personas/ # Persona loading logic
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Built-in biographical fact categories that Ei tracks.
3
+ *
4
+ * BUILT_IN_FACTS: Array of fact objects (name field only) for iteration/display.
5
+ * BUILT_IN_FACT_NAMES: Set<string> for O(1) lookup (is this fact built-in?).
6
+ */
7
+
8
+ export const BUILT_IN_FACTS: { name: string }[] = [
9
+ // Core Identity
10
+ { name: "Full Name" },
11
+ { name: "Nickname/Preferred Name" },
12
+ { name: "Birthday" },
13
+ { name: "Birthplace" },
14
+ { name: "Hometown" },
15
+ { name: "Current Location" },
16
+
17
+ // Professional
18
+ { name: "Current Job Title" },
19
+ { name: "Current Employer" },
20
+ { name: "Industry/Field" },
21
+ { name: "Years of Experience" },
22
+
23
+ // Personal
24
+ { name: "Marital Status" },
25
+ { name: "Spouse Name" },
26
+ { name: "Spouse Birthday" },
27
+ { name: "Date of Marriage" },
28
+ { name: "Children" },
29
+ { name: "Parents" },
30
+ { name: "Gender" },
31
+ { name: "Pronouns" },
32
+ { name: "Eye Color" },
33
+ { name: "Hair Color" },
34
+ { name: "Height" },
35
+ { name: "Weight" },
36
+
37
+ // Background
38
+ { name: "Nationality/Citizenship" },
39
+ { name: "Languages Spoken" },
40
+ { name: "Education Level" },
41
+ { name: "School/University" },
42
+ { name: "Field of Study" },
43
+ { name: "Military Service" },
44
+ { name: "Religious Affiliation" },
45
+ ];
46
+
47
+ export const BUILT_IN_FACT_NAMES: Set<string> = new Set(
48
+ BUILT_IN_FACTS.map((f) => f.name)
49
+ );
@@ -0,0 +1 @@
1
+ export * from "./built-in-facts";
@@ -49,7 +49,6 @@ export function stripHumanEmbeddings(human: HumanEntity): HumanEntity {
49
49
  return {
50
50
  ...human,
51
51
  facts: (human.facts ?? []).map(stripDataItemEmbedding),
52
- traits: (human.traits ?? []).map(stripDataItemEmbedding),
53
52
  topics: (human.topics ?? []).map(stripDataItemEmbedding),
54
53
  people: (human.people ?? []).map(stripDataItemEmbedding),
55
54
  quotes: (human.quotes ?? []).map(stripQuoteEmbedding),
@@ -54,6 +54,14 @@ export function getItemEmbeddingText(item: { name: string; description?: string
54
54
  return item.name;
55
55
  }
56
56
 
57
+ export function getTopicEmbeddingText(topic: { name: string; category?: string; description?: string }): string {
58
+ return [topic.name, topic.category, topic.description].filter(Boolean).join(' - ');
59
+ }
60
+
61
+ export function getPersonEmbeddingText(person: { name: string; relationship?: string; description?: string }): string {
62
+ return [person.name, person.relationship, person.description].filter(Boolean).join(' - ');
63
+ }
64
+
57
65
  export function needsEmbeddingUpdate(
58
66
  existing: { name: string; description?: string } | undefined,
59
67
  incoming: { name: string; description?: string }
@@ -1,7 +1,7 @@
1
1
  import { StateManager } from "../state-manager.js";
2
2
  import { LLMResponse } from "../types.js";
3
3
  import type { DedupResult } from "../../prompts/ceremony/types.js";
4
- import type { DataItemType, Fact, Trait, Topic, Person, Quote } from "../types/data-items.js";
4
+ import type { DataItemType, Fact, Topic, Person, Quote } from "../types/data-items.js";
5
5
  import { getEmbeddingService } from "../embedding-service.js";
6
6
 
7
7
  /**
@@ -23,6 +23,12 @@ export async function handleDedupCurate(
23
23
  const entity_ids = response.request.data.entity_ids as string[];
24
24
  const state = stateManager.getHuman();
25
25
 
26
+ // Validate entity_type
27
+ if (!entity_type || !['fact', 'topic', 'person'].includes(entity_type)) {
28
+ console.error(`[Dedup] Invalid entity_type: "${entity_type}" (from request data)`, response.request.data);
29
+ return;
30
+ }
31
+
26
32
  // Parse Opus response
27
33
  let decisions: DedupResult;
28
34
  try {
@@ -43,11 +49,29 @@ export async function handleDedupCurate(
43
49
 
44
50
  console.log(`[Dedup] Processing cluster: ${decisions.update.length} updates, ${decisions.remove.length} removals, ${decisions.add.length} additions`);
45
51
 
46
- // HYDRATION: Fetch entities by ID (graceful degradation for missing)
47
- const entityList = state[`${entity_type}s` as 'facts' | 'traits' | 'topics' | 'people'];
52
+ // Map entity_type to pluralized state property name
53
+ const pluralMap: Record<string, 'facts' | 'topics' | 'people'> = {
54
+ fact: 'facts',
55
+ topic: 'topics',
56
+ person: 'people'
57
+ };
58
+ const entityList = state[pluralMap[entity_type]];
59
+
60
+ // Validate entityList exists
61
+ if (!entityList || !Array.isArray(entityList)) {
62
+ console.error(`[Dedup] entityList is ${entityList === undefined ? 'undefined' : 'not an array'} for entity_type="${entity_type}" (looking for state.${entity_type}s)`, {
63
+ entity_type,
64
+ entity_ids,
65
+ stateKeys: Object.keys(state),
66
+ factsExists: !!state.facts,
67
+ topicsExists: !!state.topics,
68
+ peopleExists: !!state.people
69
+ });
70
+ return;
71
+ }
48
72
  const entities = entity_ids
49
- .map((id: string) => entityList.find((e: Fact | Trait | Topic | Person) => e.id === id))
50
- .filter((e: Fact | Trait | Topic | Person | undefined): e is (Fact | Trait | Topic | Person) => e !== undefined);
73
+ .map((id: string) => entityList.find((e: Fact | Topic | Person) => e.id === id))
74
+ .filter((e: Fact | Topic | Person | undefined): e is (Fact | Topic | Person) => e !== undefined);
51
75
 
52
76
  if (entities.length === 0) {
53
77
  console.warn(`[Dedup] No entities found for cluster (already merged?)`);
@@ -83,7 +107,7 @@ export async function handleDedupCurate(
83
107
  // =========================================================================
84
108
 
85
109
  for (const update of decisions.update) {
86
- const entity = entityList.find((e: Fact | Trait | Topic | Person) => e.id === update.id);
110
+ const entity = entityList.find((e: Fact | Topic | Person) => e.id === update.id);
87
111
 
88
112
  if (!entity) {
89
113
  console.warn(`[Dedup] Entity ${update.id} not found (already merged?)`);
@@ -122,8 +146,6 @@ export async function handleDedupCurate(
122
146
  // Type-safe cast based on entity_type
123
147
  if (entity_type === 'fact') {
124
148
  stateManager.human_fact_upsert(updatedEntity as Fact);
125
- } else if (entity_type === 'trait') {
126
- stateManager.human_trait_upsert(updatedEntity as Trait);
127
149
  } else if (entity_type === 'topic') {
128
150
  stateManager.human_topic_upsert(updatedEntity as Topic);
129
151
  } else if (entity_type === 'person') {
@@ -137,7 +159,7 @@ export async function handleDedupCurate(
137
159
  // =========================================================================
138
160
 
139
161
  for (const removal of decisions.remove) {
140
- const entity = entityList.find((e: Fact | Trait | Topic | Person) => e.id === removal.to_be_removed);
162
+ const entity = entityList.find((e: Fact | Topic | Person) => e.id === removal.to_be_removed);
141
163
 
142
164
  if (!entity) {
143
165
  console.warn(`[Dedup] Entity ${removal.to_be_removed} already deleted`);
@@ -146,7 +168,7 @@ export async function handleDedupCurate(
146
168
 
147
169
  // Remove via StateManager (also cleans up quote references)
148
170
  const removeMethod = `human_${entity_type}_remove` as
149
- 'human_fact_remove' | 'human_trait_remove' | 'human_topic_remove' | 'human_person_remove';
171
+ 'human_fact_remove' | 'human_topic_remove' | 'human_person_remove';
150
172
 
151
173
  const removed = stateManager[removeMethod](removal.to_be_removed);
152
174
  if (removed) {
@@ -180,12 +202,12 @@ export async function handleDedupCurate(
180
202
  description: addition.description,
181
203
  sentiment: addition.sentiment ?? 0.0,
182
204
  last_updated: new Date().toISOString(),
205
+ learned_by: "ei",
206
+ last_changed_by: "ei",
183
207
  embedding,
184
208
  // Type-specific fields with defaults
185
- ...(entity_type === 'trait' && { strength: addition.strength ?? 0.5 }),
186
209
  ...(entity_type === 'fact' && {
187
210
  confidence: addition.confidence ?? 0.5,
188
- validated: 'unknown' as import("../types/enums.js").ValidationLevel,
189
211
  validated_date: ''
190
212
  }),
191
213
  ...((entity_type === 'topic' || entity_type === 'person') && {
@@ -200,8 +222,6 @@ export async function handleDedupCurate(
200
222
  // Type-safe cast based on entity_type
201
223
  if (entity_type === 'fact') {
202
224
  stateManager.human_fact_upsert(newEntity as Fact);
203
- } else if (entity_type === 'trait') {
204
- stateManager.human_trait_upsert(newEntity as Trait);
205
225
  } else if (entity_type === 'topic') {
206
226
  stateManager.human_topic_upsert(newEntity as Topic);
207
227
  } else if (entity_type === 'person') {
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  ContextStatus,
3
3
  LLMNextStep,
4
- ValidationLevel,
5
4
  type LLMResponse,
6
5
  type Message,
7
6
  } from "../types.js";
@@ -77,13 +76,13 @@ export function handleEiHeartbeat(response: LLMResponse, state: StateManager): v
77
76
  timestamp: now,
78
77
  read: false,
79
78
  context_status: ContextStatus.Default,
80
- f: true, r: true, p: true, o: true,
79
+ f: true, t: true, p: true,
81
80
  });
82
81
 
83
82
  if (found.type === "fact") {
84
83
  const factsNav = isTUI ? "using /me facts" : "using \u2630 \u2192 My Data";
85
84
  sendMessage(`Another persona updated a fact called "${found.name}" to "${found.description}". If that's right, you can lock it from further changes by ${factsNav}.`);
86
- state.human_fact_upsert({ ...found, validated: ValidationLevel.Ei, validated_date: now });
85
+ state.human_fact_upsert({ ...found, validated_date: now });
87
86
  console.log(`[handleEiHeartbeat] Notified about fact "${found.name}"`);
88
87
  return;
89
88
  }