ei-tui 0.3.6 → 0.3.7

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.
package/README.md CHANGED
@@ -124,7 +124,7 @@ Personas can use tools. Not just read-from-memory tools — *actual* tools. Web
124
124
 
125
125
  | Tool | What it does |
126
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. |
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. Supports the `persona` filter to scope results to what a specific persona has learned. |
128
128
  | `file_read` | Read a file from your local filesystem *(TUI only)* |
129
129
  | `list_directory` | Explore folder structure *(TUI only)* |
130
130
  | `directory_tree` | Recursive directory tree *(TUI only)* |
@@ -188,11 +188,21 @@ Without this setting, browser security policies will block API calls.
188
188
 
189
189
  ## Development
190
190
 
191
+ To run the full test suite on a new machine:
192
+
191
193
  ```bash
194
+ nvm install 20
195
+ nvm use 20
196
+ npm install
197
+ cd web && npm install && npx playwright install && cd ..
198
+ cd tui
199
+ bun install
200
+ npm install
201
+ npm rebuild # compile native PTY module for Node 20 (one-time, new machine only)
202
+ cd ..
203
+ nvm use default
192
204
  npm install
193
- npm run dev # Watch mode
194
- npm run build # Compile TypeScript
195
- npm run test # Run tests
205
+ npm run test:all
196
206
  ```
197
207
 
198
208
  ## Releases
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ei-tui",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "author": "Flare576",
5
5
  "repository": {
6
6
  "type": "git",
package/src/cli/README.md CHANGED
@@ -9,6 +9,7 @@ ei facts -n 5 "query string" # Return up to 5 facts
9
9
  ei people -n 5 "query string" # Return up to 5 people
10
10
  ei topics -n 5 "query string" # Return up to 5 topics
11
11
  ei quotes -n 5 "query string" # Return up to 5 quotes
12
+ ei --persona "Beta" "query string" # Filter results to what Beta has learned
12
13
  ei --id <id> # Look up a specific entity by ID
13
14
  echo <id> | ei --id # Look up entity by ID from stdin
14
15
  ei --install # Register Ei with OpenCode, Claude Code, and Cursor
@@ -68,7 +69,7 @@ ei "What are the user's current preferences, active projects, and workflow?"
68
69
 
69
70
  Ei is a persistent knowledge base built from the user's conversations — facts, preferences,
70
71
  people, topics. Use it when the user references past work, mentions how they like things done,
71
- or asks "how did we do X." Query again mid-session when they correct you or reference something
72
+ or asks "how did we do X." Use `ei --persona "Beta" "walruses"` to scope results to what a specific persona has learned. Query again mid-session when they correct you or reference something
72
73
  from a previous session.
73
74
  ```
74
75
 
@@ -79,6 +80,7 @@ Add to `~/.claude/CLAUDE.md` (user-level) or `CLAUDE.md` at project root:
79
80
  ```markdown
80
81
  At session start, use the **ei** MCP to pull user context: call `ei_search` with a
81
82
  natural-language query about the user's preferences, active projects, and workflow.
83
+ A `persona` filter is available to scope results to what a specific persona has learned.
82
84
 
83
85
  Use Ei when the user references past decisions, mentions people or preferences, or asks
84
86
  "how did we do X." Query again when they correct you or reference something from a previous
@@ -109,7 +111,7 @@ conversations (facts, people, topics, quotes).
109
111
 
110
112
  **How to use:**
111
113
  1. Call `ei_search` (server `user-ei`) with a natural-language query; optionally filter by
112
- `type`: facts, people, topics, quotes.
114
+ `type` (facts, people, topics, quotes) or `persona` display_name.
113
115
  2. If you need full detail for a result, call `ei_lookup` with the entity `id` from step 1.
114
116
 
115
117
  Prefer querying Ei before asking the user for context they may have already shared.
@@ -122,6 +124,7 @@ The installed tool gives OpenCode agents access to all four data types with prop
122
124
  | Arg | Type | Description |
123
125
  |-----|------|-------------|
124
126
  | `query` | string (required) | Search text, or entity ID when `lookup=true` |
127
+ | `persona` | string (optional) | Persona display_name to filter results — only returns entities that persona has extracted |
125
128
  | `type` | enum (optional) | `facts` \| `people` \| `topics` \| `quotes` — omit for balanced results |
126
129
  | `limit` | number (optional) | Max results, default 10 |
127
130
  | `lookup` | boolean (optional) | If true, fetch single entity by ID |
package/src/cli/mcp.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { z } from "zod";
4
- import { retrieveBalanced, lookupById } from "./retrieval.js";
4
+ import { retrieveBalanced, lookupById, loadLatestState, type BalancedResult } from "./retrieval.js";
5
+ import type { StorageState } from "../core/types/index.js";
6
+ import { resolvePersonaId, filterByPersona, filterTypeSpecificByPersona } from "./persona-filter.js";
5
7
 
6
8
  // Exported so tests can inject their own transport
7
9
  export function createMcpServer(): McpServer {
@@ -23,6 +25,12 @@ export function createMcpServer(): McpServer {
23
25
  .describe(
24
26
  "Filter to a specific data type. Omit to search all types (balanced across all 4)."
25
27
  ),
28
+ persona: z
29
+ .string()
30
+ .optional()
31
+ .describe(
32
+ "Filter to entities a specific persona has learned about. Use the persona display name."
33
+ ),
26
34
  limit: z
27
35
  .number()
28
36
  .optional()
@@ -34,16 +42,36 @@ export function createMcpServer(): McpServer {
34
42
  .describe("If true, sort by most recently mentioned."),
35
43
  },
36
44
  },
37
- async ({ query, type, limit, recent }) => {
45
+ async ({ query, type, persona, limit, recent }) => {
38
46
  const options = { recent: recent ?? false };
39
47
  const effectiveLimit = limit ?? 10;
40
48
 
49
+ let state: StorageState | null = null;
50
+ let personaId: string | undefined;
51
+ if (persona) {
52
+ state = await loadLatestState();
53
+ if (state) {
54
+ personaId = resolvePersonaId(state, persona) ?? undefined;
55
+ if (!personaId) {
56
+ return {
57
+ content: [{ type: "text" as const, text: `Persona "${persona}" not found.` }],
58
+ };
59
+ }
60
+ }
61
+ }
62
+
41
63
  let result: unknown;
42
64
  if (type) {
43
65
  const module = await import(`./commands/${type}.js`);
44
66
  result = await (module.execute as (q: string, l: number, o: { recent: boolean }) => Promise<unknown>)(query, effectiveLimit, options);
67
+ if (personaId && state) {
68
+ result = filterTypeSpecificByPersona(result as { id: string }[], state, personaId, type);
69
+ }
45
70
  } else {
46
71
  result = await retrieveBalanced(query, effectiveLimit, options);
72
+ if (personaId && state) {
73
+ result = filterByPersona(result as BalancedResult[], state, personaId);
74
+ }
47
75
  }
48
76
 
49
77
  return {
@@ -0,0 +1,54 @@
1
+ import type { StorageState } from "../core/types/index.js";
2
+ import type { BalancedResult } from "./retrieval.js";
3
+
4
+ export function resolvePersonaId(state: StorageState, name: string): string | null {
5
+ const lowerName = name.toLowerCase();
6
+ for (const { entity } of Object.values(state.personas)) {
7
+ if (entity.display_name.toLowerCase() === lowerName) {
8
+ return entity.id;
9
+ }
10
+ }
11
+ return null;
12
+ }
13
+
14
+ export function filterByPersona(results: BalancedResult[], state: StorageState, personaId: string): BalancedResult[] {
15
+ return results.filter((result) => {
16
+ if (result.type === "quote") {
17
+ return false;
18
+ }
19
+ const { id } = result;
20
+ let original: { interested_personas?: string[] } | undefined;
21
+ if (result.type === "fact") {
22
+ original = state.human.facts.find((f) => f.id === id);
23
+ } else if (result.type === "topic") {
24
+ original = state.human.topics.find((t) => t.id === id);
25
+ } else if (result.type === "person") {
26
+ original = state.human.people.find((p) => p.id === id);
27
+ }
28
+ return original?.interested_personas?.includes(personaId) ?? false;
29
+ });
30
+ }
31
+
32
+ export function filterTypeSpecificByPersona<T extends { id: string }>(
33
+ results: T[],
34
+ state: StorageState,
35
+ personaId: string,
36
+ targetType: string
37
+ ): T[] {
38
+ if (targetType === "quotes") {
39
+ return [];
40
+ }
41
+ const collection =
42
+ targetType === "facts"
43
+ ? state.human.facts
44
+ : targetType === "topics"
45
+ ? state.human.topics
46
+ : targetType === "people"
47
+ ? state.human.people
48
+ : null;
49
+ if (!collection) return results;
50
+ return results.filter((r) => {
51
+ const original = collection.find((item) => item.id === r.id) as { interested_personas?: string[] } | undefined;
52
+ return original?.interested_personas?.includes(personaId) ?? false;
53
+ });
54
+ }
package/src/cli.ts CHANGED
@@ -13,7 +13,9 @@
13
13
 
14
14
  import { parseArgs } from "util";
15
15
  import { join } from "path";
16
- import { retrieveBalanced, lookupById } from "./cli/retrieval";
16
+ import { retrieveBalanced, lookupById, loadLatestState } from "./cli/retrieval";
17
+ import type { StorageState } from "./core/types";
18
+ import { resolvePersonaId, filterByPersona, filterTypeSpecificByPersona } from "./cli/persona-filter.js";
17
19
 
18
20
  const TYPE_ALIASES: Record<string, string> = {
19
21
  quote: "quotes",
@@ -39,6 +41,7 @@ Usage:
39
41
  ei --recent Return most recently mentioned items
40
42
  ei --recent "query" Filter recent items by query
41
43
  ei <type> --recent "query" Type-specific recent search
44
+ ei --persona "Name" "query" Filter results to what a persona has learned
42
45
  ei --id <id> Look up a specific entity by ID
43
46
  echo <id> | ei --id Look up entity by ID from stdin
44
47
  ei mcp Start the Ei MCP stdio server (for Cursor/Claude Desktop)
@@ -52,6 +55,7 @@ Types:
52
55
  Options:
53
56
  --number, -n Maximum number of results (default: 10)
54
57
  --recent, -r Sort by last_mentioned date (most recent first)
58
+ --persona, -p Filter to entities a specific persona has learned about
55
59
  --id Look up entity by ID (accepts value or stdin)
56
60
  --install Register Ei with OpenCode, Claude Code, and Cursor
57
61
  --help, -h Show this help message
@@ -62,6 +66,7 @@ Examples:
62
66
  ei quote "you guessed it" # Search quotes only
63
67
  ei --recent # Most recently mentioned items
64
68
  ei topics --recent "work" # Recent work-related topics
69
+ ei --persona "Architect" "work stuff" # What Architect knows about work
65
70
  ei --id abc-123 # Look up entity by ID
66
71
  ei "memory leak" | jq .[0].id | ei --id # Pipe ID from search
67
72
  `);
@@ -88,6 +93,12 @@ function buildOpenCodeToolContent(): string {
88
93
  ' .describe(',
89
94
  ' "Filter to a specific data type. Omit to search all types (balanced across all 4).",',
90
95
  ' ),',
96
+ ' persona: tool.schema',
97
+ ' .string()',
98
+ ' .optional()',
99
+ ' .describe(',
100
+ ' "Filter to entities a specific persona has learned about. Use the persona display name.",',
101
+ ' ),',
91
102
  ' limit: tool.schema',
92
103
  ' .number()',
93
104
  ' .int()',
@@ -108,6 +119,7 @@ function buildOpenCodeToolContent(): string {
108
119
  ' cmd.push("--id", args.query);',
109
120
  ' } else {',
110
121
  ' if (args.type) cmd.push(args.type);',
122
+ ' if (args.persona) cmd.push("--persona", args.persona);',
111
123
  ' if (args.limit && args.limit !== 10) cmd.push("-n", String(args.limit));',
112
124
  ' cmd.push(args.query);',
113
125
  ' }',
@@ -292,6 +304,7 @@ async function main(): Promise<void> {
292
304
  options: {
293
305
  number: { type: "string", short: "n" },
294
306
  recent: { type: "boolean", short: "r" },
307
+ persona: { type: "string", short: "p" },
295
308
  help: { type: "boolean", short: "h" },
296
309
  },
297
310
  allowPositionals: true,
@@ -310,6 +323,7 @@ async function main(): Promise<void> {
310
323
  const query = parsed.positionals.join(" ").trim();
311
324
  const limit = parsed.values.number ? parseInt(parsed.values.number, 10) : 10;
312
325
  const recent = parsed.values.recent === true;
326
+ const personaName = parsed.values.persona?.trim();
313
327
 
314
328
  if (!query && !recent) {
315
329
  if (targetType) {
@@ -325,14 +339,35 @@ async function main(): Promise<void> {
325
339
  process.exit(1);
326
340
  }
327
341
 
342
+ let state: StorageState | null = null;
343
+ let personaId: string | undefined;
344
+ if (personaName) {
345
+ state = await loadLatestState();
346
+ if (!state) {
347
+ console.error("No saved state found. Is EI_DATA_PATH set correctly?");
348
+ process.exit(1);
349
+ }
350
+ personaId = resolvePersonaId(state, personaName) ?? undefined;
351
+ if (!personaId) {
352
+ console.error(`Persona "${personaName}" not found.`);
353
+ process.exit(1);
354
+ }
355
+ }
356
+
328
357
  const options = { recent };
329
358
 
330
359
  let result;
331
360
  if (targetType) {
332
361
  const module = await import(`./cli/commands/${targetType}.js`);
333
362
  result = await module.execute(query, limit, options);
363
+ if (personaId && state) {
364
+ result = filterTypeSpecificByPersona(result, state, personaId, targetType);
365
+ }
334
366
  } else {
335
367
  result = await retrieveBalanced(query, limit, options);
368
+ if (personaId && state) {
369
+ result = filterByPersona(result, state, personaId);
370
+ }
336
371
  }
337
372
 
338
373
  console.log(JSON.stringify(result, null, 2));
@@ -72,6 +72,7 @@ export async function handleFactFind(response: LLMResponse, state: StateManager)
72
72
  last_mentioned: now,
73
73
  learned_by: existingFact.learned_by ?? context.personaId,
74
74
  last_changed_by: context.personaId,
75
+ interested_personas: [...new Set([...(existingFact.interested_personas ?? []), context.personaId])],
75
76
  embedding,
76
77
  };
77
78
 
@@ -165,6 +165,9 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
165
165
  last_mentioned: now,
166
166
  learned_by: isNewItem ? personaId : existingTopic?.learned_by,
167
167
  last_changed_by: personaId,
168
+ interested_personas: isNewItem
169
+ ? [personaId]
170
+ : [...new Set([...(existingTopic?.interested_personas ?? []), personaId])],
168
171
  persona_groups: mergeGroups(personaGroup, isNewItem, existingTopic?.persona_groups),
169
172
  embedding,
170
173
  };
@@ -232,6 +235,9 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
232
235
  last_mentioned: now,
233
236
  learned_by: isNewItem ? personaId : existingPerson?.learned_by,
234
237
  last_changed_by: personaId,
238
+ interested_personas: isNewItem
239
+ ? [personaId]
240
+ : [...new Set([...(existingPerson?.interested_personas ?? []), personaId])],
235
241
  persona_groups: mergeGroups(personaGroup, isNewItem, existingPerson?.persona_groups),
236
242
  embedding,
237
243
  };
@@ -145,14 +145,14 @@ export async function getQuotesForMessage(sm: StateManager, messageId: string):
145
145
  export async function searchHumanData(
146
146
  sm: StateManager,
147
147
  query: string,
148
- options: { types?: Array<"fact" | "topic" | "person" | "quote">; limit?: number; recent?: boolean } = {}
148
+ options: { types?: Array<"fact" | "topic" | "person" | "quote">; limit?: number; recent?: boolean; persona_filter?: string } = {}
149
149
  ): Promise<{
150
150
  facts: Fact[];
151
151
  topics: Topic[];
152
152
  people: Person[];
153
153
  quotes: Quote[];
154
154
  }> {
155
- const { types = ["fact", "topic", "person", "quote"], limit = 10, recent } = options;
155
+ const { types = ["fact", "topic", "person", "quote"], limit = 10, recent, persona_filter } = options;
156
156
  const human = sm.getHuman();
157
157
  const SIMILARITY_THRESHOLD = 0.3;
158
158
 
@@ -214,18 +214,30 @@ export async function searchHumanData(
214
214
  };
215
215
 
216
216
  if (types.includes("fact")) {
217
- result.facts = searchItems(human.facts, (f) => `${f.name} ${f.description || ""}`).map(
217
+ let facts = human.facts;
218
+ if (persona_filter) {
219
+ facts = facts.filter(f => f.interested_personas?.includes(persona_filter));
220
+ }
221
+ result.facts = searchItems(facts, (f) => `${f.name} ${f.description || ""}`).map(
218
222
  stripDataItemEmbedding
219
223
  );
220
224
  }
221
225
  if (types.includes("topic")) {
222
- result.topics = searchItems(human.topics, (t) => `${t.name} ${t.description || ""}`).map(
226
+ let topics = human.topics;
227
+ if (persona_filter) {
228
+ topics = topics.filter(t => t.interested_personas?.includes(persona_filter));
229
+ }
230
+ result.topics = searchItems(topics, (t) => `${t.name} ${t.description || ""}`).map(
223
231
  stripDataItemEmbedding
224
232
  );
225
233
  }
226
234
  if (types.includes("person")) {
235
+ let people = human.people;
236
+ if (persona_filter) {
237
+ people = people.filter(p => p.interested_personas?.includes(persona_filter));
238
+ }
227
239
  result.people = searchItems(
228
- human.people,
240
+ people,
229
241
  (p) => `${p.name} ${p.description || ""} ${p.relationship}`
230
242
  ).map(stripDataItemEmbedding);
231
243
  }
@@ -203,7 +203,7 @@ export class Processor {
203
203
  this.bootstrapTools();
204
204
  this.seedBuiltinFacts();
205
205
  this.seedSettings();
206
- registerReadMemoryExecutor(createReadMemoryExecutor(this.searchHumanData.bind(this)));
206
+ registerReadMemoryExecutor(createReadMemoryExecutor(this.searchHumanData.bind(this), this.getPersonaList.bind(this)));
207
207
  if (this.isTUI) {
208
208
  await registerFileReadExecutor();
209
209
  }
@@ -1485,7 +1485,7 @@ const toolNextSteps = new Set([
1485
1485
 
1486
1486
  async searchHumanData(
1487
1487
  query: string,
1488
- options: { types?: Array<"fact" | "topic" | "person" | "quote">; limit?: number; recent?: boolean } = {}
1488
+ options: { types?: Array<"fact" | "topic" | "person" | "quote">; limit?: number; recent?: boolean; persona_filter?: string } = {}
1489
1489
  ): Promise<{
1490
1490
  facts: Fact[];
1491
1491
  topics: Topic[];
@@ -46,6 +46,7 @@ export class StateManager {
46
46
  this.migrateLearnedByToIds();
47
47
  this.migrateFactValidation();
48
48
  this.migrateMessageFlags();
49
+ this.migrateInterestedPersonas();
49
50
  } else {
50
51
  this.humanState.load(createDefaultHumanEntity());
51
52
  }
@@ -191,6 +192,29 @@ export class StateManager {
191
192
  }
192
193
  }
193
194
 
195
+ /**
196
+ * Migration: interested_personas was added to DataItemBase.
197
+ * On load, backfill from learned_by + last_changed_by for any item missing the field.
198
+ */
199
+ private migrateInterestedPersonas(): void {
200
+ const human = this.humanState.get();
201
+ let dirty = false;
202
+
203
+ const migrateItem = (item: { learned_by?: string; last_changed_by?: string; interested_personas?: string[] }) => {
204
+ if (item.interested_personas === undefined || item.interested_personas === null) {
205
+ item.interested_personas = [...new Set([item.learned_by, item.last_changed_by].filter(Boolean) as string[])];
206
+ dirty = true;
207
+ }
208
+ };
209
+
210
+ [...human.facts, ...human.topics, ...human.people].forEach(migrateItem);
211
+
212
+ if (dirty) {
213
+ this.humanState.set(human);
214
+ console.log("[StateManager] Migrated interested_personas fields from learned_by + last_changed_by");
215
+ }
216
+ }
217
+
194
218
  /**
195
219
  * Returns true if value looks like a persona ID (UUID or the special "ei" id).
196
220
  * Display names are free-form strings that won't match UUID format.
@@ -1,19 +1,27 @@
1
1
  import type { ToolExecutor } from "../types.js";
2
2
  import type { Fact, Topic, Person, Quote } from "../../types.js";
3
3
 
4
+ interface PersonaSummary {
5
+ id: string;
6
+ display_name: string;
7
+ }
8
+
4
9
  type SearchHumanData = (
5
10
  query: string,
6
- options?: { types?: Array<"fact" | "topic" | "person" | "quote">; limit?: number; recent?: boolean }
11
+ options?: { types?: Array<"fact" | "topic" | "person" | "quote">; limit?: number; recent?: boolean; persona_filter?: string }
7
12
  ) => Promise<{ facts: Fact[]; topics: Topic[]; people: Person[]; quotes: Quote[] }>;
8
13
 
9
- export function createReadMemoryExecutor(searchHumanData: SearchHumanData): ToolExecutor {
14
+ type GetPersonaList = () => Promise<PersonaSummary[]>;
15
+
16
+ export function createReadMemoryExecutor(searchHumanData: SearchHumanData, getPersonaList?: GetPersonaList): ToolExecutor {
10
17
  return {
11
18
  name: "read_memory",
12
19
 
13
20
  async execute(args: Record<string, unknown>): Promise<string> {
14
21
  const query = typeof args.query === "string" ? args.query.trim() : "";
15
22
  const recent = args.recent === true;
16
- console.log(`[read_memory] called with query="${query}", types=${JSON.stringify(args.types ?? null)}, limit=${args.limit ?? 10}, recent=${recent}`);
23
+ const personaArg = typeof args.persona === "string" ? args.persona.trim() : "";
24
+ console.log(`[read_memory] called with query="${query}", types=${JSON.stringify(args.types ?? null)}, limit=${args.limit ?? 10}, recent=${recent}, persona="${personaArg}"`);
17
25
 
18
26
  if (!query && !recent) {
19
27
  console.warn("[read_memory] missing query argument");
@@ -28,7 +36,20 @@ export function createReadMemoryExecutor(searchHumanData: SearchHumanData): Tool
28
36
 
29
37
  const limit = typeof args.limit === "number" && args.limit > 0 ? Math.min(args.limit, 20) : 10;
30
38
 
31
- const results = await searchHumanData(query, { types, limit, recent });
39
+ // Resolve persona display_name to ID
40
+ let persona_filter: string | undefined;
41
+ if (personaArg && getPersonaList) {
42
+ const personas = await getPersonaList();
43
+ const match = personas.find(p => p.display_name.toLowerCase() === personaArg.toLowerCase());
44
+ if (match) {
45
+ persona_filter = match.id;
46
+ console.log(`[read_memory] resolved persona "${personaArg}" to ID "${persona_filter}"`);
47
+ } else {
48
+ console.warn(`[read_memory] persona "${personaArg}" not found, proceeding without filter`);
49
+ }
50
+ }
51
+
52
+ const results = await searchHumanData(query, { types, limit, recent, persona_filter });
32
53
 
33
54
  const total = results.facts.length + results.topics.length + results.people.length + results.quotes.length;
34
55
  console.log(`[read_memory] query="${query}" => ${total} results (facts=${results.facts.length}, topics=${results.topics.length}, people=${results.people.length}, quotes=${results.quotes.length})`);
@@ -13,6 +13,7 @@ export interface DataItemBase {
13
13
  last_mentioned?: string; // Set by extraction only, never ceremony. Used for --recent sorting.
14
14
  learned_by?: string; // Persona ID that originally learned this item (stable UUID)
15
15
  last_changed_by?: string; // Persona ID that most recently updated this item (stable UUID)
16
+ interested_personas?: string[]; // Persona IDs that have extracted/touched this item (accumulated)
16
17
  persona_groups?: string[];
17
18
  embedding?: number[];
18
19
  rewrite_checked?: boolean; // True after rewrite scan finds no changes. Cleared automatically when extraction upserts a fresh item.