ei-tui 0.1.17 → 0.1.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ei-tui",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "author": "Flare576",
5
5
  "repository": {
6
6
  "type": "git",
@@ -201,8 +201,17 @@ function createBunService(): EmbeddingService {
201
201
  if (embedderPromise) return embedderPromise;
202
202
 
203
203
  embedderPromise = (async () => {
204
- const mod = await import(/* @vite-ignore */ FASTEMBED_MODULE);
205
- embedder = await mod.FlagEmbedding.init({ model: mod.EmbeddingModel.AllMiniLML6V2 });
204
+ const [mod, os, path] = await Promise.all([
205
+ import(/* @vite-ignore */ FASTEMBED_MODULE),
206
+ import('os'),
207
+ import('path'),
208
+ ]);
209
+ // Use EI_DATA_PATH if set, otherwise fall back to ~/.local/share/ei/embeddings.
210
+ // Must be absolute so the cache is stable regardless of cwd.
211
+ const cacheDir = process.env.EI_DATA_PATH
212
+ ? path.join(process.env.EI_DATA_PATH, 'embeddings')
213
+ : path.join(os.homedir(), '.local', 'share', 'ei', 'embeddings');
214
+ embedder = await mod.FlagEmbedding.init({ model: mod.EmbeddingModel.AllMiniLML6V2, cacheDir });
206
215
  return embedder;
207
216
  })();
208
217
 
@@ -29,8 +29,13 @@ import type {
29
29
  PersonaExpireResult,
30
30
  PersonaExploreResult,
31
31
  DescriptionCheckResult,
32
+ RewriteItemType,
33
+ RewriteScanResult,
34
+ RewriteResult,
35
+ RewriteSubjectMatch,
32
36
  } from "../../prompts/ceremony/types.js";
33
- import {
37
+ import { buildRewritePrompt } from "../../prompts/ceremony/rewrite.js";
38
+ import {
34
39
  orchestratePersonaGeneration,
35
40
  queueItemMatch,
36
41
  queueItemUpdate,
@@ -59,6 +64,30 @@ import { crossFind } from "../utils/index.js";
59
64
 
60
65
  export type ResponseHandler = (response: LLMResponse, state: StateManager) => void | Promise<void>;
61
66
 
67
+ // =============================================================================
68
+ // REWRITE HANDLER INJECTION
69
+ // searchHumanData lives on Processor — inject it at startup to avoid circular deps.
70
+ // Same pattern as registerReadMemoryExecutor in tools/index.ts.
71
+ // =============================================================================
72
+
73
+ type SearchHumanDataFn = (
74
+ query: string,
75
+ options?: { types?: Array<'fact' | 'trait' | 'topic' | 'person' | 'quote'>; limit?: number }
76
+ ) => Promise<{
77
+ facts: Fact[];
78
+ traits: Trait[];
79
+ topics: Topic[];
80
+ people: Person[];
81
+ quotes: Quote[];
82
+ }>;
83
+
84
+ let _searchHumanData: SearchHumanDataFn | null = null;
85
+
86
+ /** Called by Processor at startup to inject searchHumanData for rewrite handlers. */
87
+ export function registerSearchHumanData(fn: SearchHumanDataFn): void {
88
+ _searchHumanData = fn;
89
+ }
90
+
62
91
  function splitMessagesByTimestamp(
63
92
  messages: Message[],
64
93
  analyzeFromTimestamp: string | null
@@ -1117,6 +1146,298 @@ function handlePersonaTopicUpdate(response: LLMResponse, state: StateManager): v
1117
1146
 
1118
1147
 
1119
1148
 
1149
+ // =============================================================================
1150
+ // REWRITE HANDLERS (Ceremony — fire-and-forget)
1151
+ // =============================================================================
1152
+
1153
+ /**
1154
+ * handleRewriteScan — Phase 1 of Rewrite.
1155
+ * LLM returns an array of subject strings found in the bloated item.
1156
+ * For each subject we search the knowledge base, then queue Phase 2.
1157
+ */
1158
+ async function handleRewriteScan(response: LLMResponse, state: StateManager): Promise<void> {
1159
+ const itemId = response.request.data.itemId as string;
1160
+ const itemType = response.request.data.itemType as RewriteItemType;
1161
+ const rewriteModel = response.request.data.rewriteModel as string;
1162
+
1163
+ if (!itemId || !itemType) {
1164
+ console.error("[handleRewriteScan] Missing itemId or itemType in request data");
1165
+ return;
1166
+ }
1167
+
1168
+ const subjects = response.parsed as RewriteScanResult | undefined;
1169
+ if (!subjects || !Array.isArray(subjects) || subjects.length === 0) {
1170
+ console.log(`[handleRewriteScan] No extra subjects found for ${itemType} "${itemId}" — item is cohesive`);
1171
+ return;
1172
+ }
1173
+
1174
+ if (!_searchHumanData) {
1175
+ console.error("[handleRewriteScan] searchHumanData not injected — cannot search for matches");
1176
+ return;
1177
+ }
1178
+
1179
+ // Re-read the item from current state (it may have changed since scan was queued)
1180
+ const human = state.getHuman();
1181
+ const allItems: DataItemBase[] = [
1182
+ ...human.facts, ...human.traits, ...human.topics, ...human.people,
1183
+ ];
1184
+ const currentItem = allItems.find(i => i.id === itemId);
1185
+ if (!currentItem) {
1186
+ console.warn(`[handleRewriteScan] Item ${itemId} no longer exists — skipping rewrite`);
1187
+ return;
1188
+ }
1189
+
1190
+ // Search for matches per subject, excluding the original item
1191
+ const subjectMatches: RewriteSubjectMatch[] = [];
1192
+ for (const searchTerm of subjects) {
1193
+ try {
1194
+ const results = await _searchHumanData(searchTerm, {
1195
+ types: ["fact", "trait", "topic", "person"],
1196
+ limit: 4, // fetch 4 so we can exclude original and still have 3
1197
+ });
1198
+ const allMatches: DataItemBase[] = [
1199
+ ...results.facts, ...results.traits, ...results.topics, ...results.people,
1200
+ ].filter(m => m.id !== itemId); // exclude original
1201
+ subjectMatches.push({ searchTerm, matches: allMatches.slice(0, 3) });
1202
+ } catch (err) {
1203
+ console.warn(`[handleRewriteScan] Search failed for "${searchTerm}":`, err);
1204
+ subjectMatches.push({ searchTerm, matches: [] });
1205
+ }
1206
+ }
1207
+
1208
+ // Build Phase 2 prompt and queue it
1209
+ const prompt = buildRewritePrompt({
1210
+ item: currentItem,
1211
+ itemType,
1212
+ subjects: subjectMatches,
1213
+ });
1214
+
1215
+ state.queue_enqueue({
1216
+ type: LLMRequestType.JSON,
1217
+ priority: LLMPriority.Normal,
1218
+ system: prompt.system,
1219
+ user: prompt.user,
1220
+ next_step: LLMNextStep.HandleRewriteRewrite,
1221
+ model: rewriteModel,
1222
+ data: {
1223
+ itemId,
1224
+ itemType,
1225
+ },
1226
+ });
1227
+
1228
+ console.log(`[handleRewriteScan] Queued Phase 2 for ${itemType} "${currentItem.name}" with ${subjectMatches.length} subject(s)`);
1229
+ }
1230
+
1231
+ /**
1232
+ * handleRewriteRewrite — Phase 2 of Rewrite.
1233
+ * LLM returns { existing: [...], new: [...] }.
1234
+ * Upsert existing items by id, create new items with defensive defaults + embeddings.
1235
+ */
1236
+ async function handleRewriteRewrite(response: LLMResponse, state: StateManager): Promise<void> {
1237
+ const itemId = response.request.data.itemId as string;
1238
+ const itemType = response.request.data.itemType as RewriteItemType;
1239
+
1240
+ if (!itemId || !itemType) {
1241
+ console.error("[handleRewriteRewrite] Missing itemId or itemType in request data");
1242
+ return;
1243
+ }
1244
+
1245
+ const result = response.parsed as RewriteResult | undefined;
1246
+ if (!result || (!result.existing?.length && !result.new?.length)) {
1247
+ console.log(`[handleRewriteRewrite] No changes returned for ${itemType} "${itemId}"`);
1248
+ return;
1249
+ }
1250
+
1251
+ const human = state.getHuman();
1252
+ const now = new Date().toISOString();
1253
+
1254
+ // Look up the original item to inherit persona_groups
1255
+ const allItems: DataItemBase[] = [
1256
+ ...human.facts, ...human.traits, ...human.topics, ...human.people,
1257
+ ];
1258
+ const originalItem = allItems.find(i => i.id === itemId);
1259
+ const inheritedGroups = originalItem?.persona_groups;
1260
+
1261
+ // Helper: resolve actual type from existing records (don't trust LLM's type field)
1262
+ const resolveExistingType = (id: string): RewriteItemType | null => {
1263
+ if (human.facts.find(f => f.id === id)) return "fact";
1264
+ if (human.traits.find(t => t.id === id)) return "trait";
1265
+ if (human.topics.find(t => t.id === id)) return "topic";
1266
+ if (human.people.find(p => p.id === id)) return "person";
1267
+ return null;
1268
+ };
1269
+
1270
+ let existingCount = 0;
1271
+ let newCount = 0;
1272
+
1273
+ // --- Process existing items ---
1274
+ for (const item of result.existing ?? []) {
1275
+ if (!item.id || !item.name || !item.description) {
1276
+ console.warn(`[handleRewriteRewrite] Skipping existing item with missing fields: ${JSON.stringify(item).slice(0, 100)}`);
1277
+ continue;
1278
+ }
1279
+
1280
+ // Resolve type from actual records, not from LLM response
1281
+ const resolvedType = resolveExistingType(item.id);
1282
+ if (!resolvedType) {
1283
+ console.warn(`[handleRewriteRewrite] Existing item id "${item.id}" not found in human data — skipping`);
1284
+ continue;
1285
+ }
1286
+
1287
+ let embedding: number[] | undefined;
1288
+ try {
1289
+ const embeddingService = getEmbeddingService();
1290
+ const text = getItemEmbeddingText({ name: item.name, description: item.description });
1291
+ embedding = await embeddingService.embed(text);
1292
+ } catch (err) {
1293
+ console.warn(`[handleRewriteRewrite] Failed to compute embedding for existing ${resolvedType} "${item.name}":`, err);
1294
+ }
1295
+
1296
+ switch (resolvedType) {
1297
+ case "fact": {
1298
+ const existing = human.facts.find(f => f.id === item.id)!;
1299
+ state.human_fact_upsert({
1300
+ ...existing,
1301
+ name: item.name,
1302
+ description: item.description,
1303
+ sentiment: item.sentiment ?? existing.sentiment,
1304
+ last_updated: now,
1305
+ embedding,
1306
+ });
1307
+ break;
1308
+ }
1309
+ case "trait": {
1310
+ const existing = human.traits.find(t => t.id === item.id)!;
1311
+ state.human_trait_upsert({
1312
+ ...existing,
1313
+ name: item.name,
1314
+ description: item.description,
1315
+ sentiment: item.sentiment ?? existing.sentiment,
1316
+ strength: item.strength ?? existing.strength,
1317
+ last_updated: now,
1318
+ embedding,
1319
+ });
1320
+ break;
1321
+ }
1322
+ case "topic": {
1323
+ const existing = human.topics.find(t => t.id === item.id)!;
1324
+ state.human_topic_upsert({
1325
+ ...existing,
1326
+ name: item.name,
1327
+ description: item.description,
1328
+ sentiment: item.sentiment ?? existing.sentiment,
1329
+ last_updated: now,
1330
+ embedding,
1331
+ });
1332
+ break;
1333
+ }
1334
+ case "person": {
1335
+ const existing = human.people.find(p => p.id === item.id)!;
1336
+ state.human_person_upsert({
1337
+ ...existing,
1338
+ name: item.name,
1339
+ description: item.description,
1340
+ sentiment: item.sentiment ?? existing.sentiment,
1341
+ last_updated: now,
1342
+ embedding,
1343
+ });
1344
+ break;
1345
+ }
1346
+ }
1347
+ existingCount++;
1348
+ }
1349
+
1350
+ // --- Process new items ---
1351
+ for (const item of result.new ?? []) {
1352
+ if (!item.type || !item.name || !item.description) {
1353
+ console.warn(`[handleRewriteRewrite] Skipping new item with missing fields: ${JSON.stringify(item).slice(0, 100)}`);
1354
+ continue;
1355
+ }
1356
+
1357
+ let embedding: number[] | undefined;
1358
+ try {
1359
+ const embeddingService = getEmbeddingService();
1360
+ const text = getItemEmbeddingText({ name: item.name, description: item.description });
1361
+ embedding = await embeddingService.embed(text);
1362
+ } catch (err) {
1363
+ console.warn(`[handleRewriteRewrite] Failed to compute embedding for new ${item.type} "${item.name}":`, err);
1364
+ }
1365
+
1366
+ const baseFields = {
1367
+ id: crypto.randomUUID(),
1368
+ name: item.name,
1369
+ description: item.description,
1370
+ sentiment: item.sentiment ?? 0,
1371
+ last_updated: now,
1372
+ learned_by: "ei",
1373
+ persona_groups: inheritedGroups,
1374
+ embedding,
1375
+ };
1376
+
1377
+ switch (item.type) {
1378
+ case "fact": {
1379
+ const fact: Fact = {
1380
+ ...baseFields,
1381
+ validated: ValidationLevel.None,
1382
+ validated_date: now,
1383
+ };
1384
+ state.human_fact_upsert(fact);
1385
+ break;
1386
+ }
1387
+ case "trait": {
1388
+ const trait: Trait = {
1389
+ ...baseFields,
1390
+ strength: item.strength ?? 0.5,
1391
+ };
1392
+ state.human_trait_upsert(trait);
1393
+ break;
1394
+ }
1395
+ case "topic": {
1396
+ if (!item.category) {
1397
+ console.warn(`[handleRewriteRewrite] New topic "${item.name}" missing category — defaulting to "Interest"`);
1398
+ }
1399
+ const topic: Topic = {
1400
+ ...baseFields,
1401
+ category: item.category ?? "Interest",
1402
+ exposure_current: 0.5,
1403
+ exposure_desired: 0.5,
1404
+ };
1405
+ state.human_topic_upsert(topic);
1406
+ break;
1407
+ }
1408
+ case "person": {
1409
+ if (!item.relationship) {
1410
+ console.warn(`[handleRewriteRewrite] New person "${item.name}" missing relationship — defaulting to "Unknown"`);
1411
+ }
1412
+ const person: Person = {
1413
+ ...baseFields,
1414
+ relationship: item.relationship ?? "Unknown",
1415
+ exposure_current: 0.5,
1416
+ exposure_desired: 0.5,
1417
+ };
1418
+ state.human_person_upsert(person);
1419
+ break;
1420
+ }
1421
+ default:
1422
+ console.warn(`[handleRewriteRewrite] Unknown type "${item.type}" for new item "${item.name}" — skipping`);
1423
+ }
1424
+ newCount++;
1425
+ }
1426
+
1427
+ console.log(`[handleRewriteRewrite] Complete for ${itemType} "${itemId}": ${existingCount} existing updated, ${newCount} new created`);
1428
+ }
1429
+
1430
+ /**
1431
+ * handleToolSynthesis — second LLM call in the tool flow.
1432
+ * The QueueProcessor already injected tool history into messages and got the
1433
+ * final persona response. Parse and store it exactly like handlePersonaResponse.
1434
+ */
1435
+ function handleToolSynthesis(response: LLMResponse, state: StateManager): void {
1436
+ console.log(`[handleToolSynthesis] Routing to handlePersonaResponse`);
1437
+ handlePersonaResponse(response, state);
1438
+ }
1439
+
1440
+
1120
1441
  export const handlers: Record<LLMNextStep, ResponseHandler> = {
1121
1442
  handlePersonaResponse,
1122
1443
  handlePersonaGeneration,
@@ -1137,4 +1458,7 @@ export const handlers: Record<LLMNextStep, ResponseHandler> = {
1137
1458
  handlePersonaExpire,
1138
1459
  handlePersonaExplore,
1139
1460
  handleDescriptionCheck,
1461
+ handleToolSynthesis,
1462
+ handleRewriteScan,
1463
+ handleRewriteRewrite,
1140
1464
  };
@@ -17,11 +17,17 @@ export interface ResolvedModel {
17
17
  export interface LLMCallOptions {
18
18
  signal?: AbortSignal;
19
19
  temperature?: number;
20
+ /** OpenAI-compatible tools array. When present and non-empty, sent with tool_choice: "auto". */
21
+ tools?: Record<string, unknown>[];
20
22
  }
21
23
 
22
24
  export interface LLMRawResponse {
23
25
  content: string | null;
24
26
  finishReason: string | null;
27
+ /** Raw tool_calls array from the API response, present when finishReason is "tool_calls". */
28
+ rawToolCalls?: unknown[];
29
+ /** The full assistant message object (needed to inject into history for the tool loop). */
30
+ assistantMessage?: Record<string, unknown>;
25
31
  }
26
32
 
27
33
  let llmCallCount = 0;
@@ -170,14 +176,21 @@ export async function callLLMRaw(
170
176
  headers["anthropic-dangerous-direct-browser-access"] = "true";
171
177
  }
172
178
 
179
+ const requestBody: Record<string, unknown> = {
180
+ model,
181
+ messages: finalMessages,
182
+ temperature,
183
+ };
184
+
185
+ if (options.tools && options.tools.length > 0) {
186
+ requestBody.tools = options.tools;
187
+ requestBody.tool_choice = "auto";
188
+ }
189
+
173
190
  const response = await fetch(`${normalizedBaseURL}/chat/completions`, {
174
191
  method: "POST",
175
192
  headers,
176
- body: JSON.stringify({
177
- model,
178
- messages: finalMessages,
179
- temperature,
180
- }),
193
+ body: JSON.stringify(requestBody),
181
194
  signal,
182
195
  });
183
196
 
@@ -189,9 +202,16 @@ export async function callLLMRaw(
189
202
  const data = await response.json();
190
203
  const choice = data.choices?.[0];
191
204
 
205
+ const assistantMessage = choice?.message as Record<string, unknown> | undefined;
206
+ const rawToolCalls = Array.isArray(choice?.message?.tool_calls)
207
+ ? (choice.message.tool_calls as unknown[])
208
+ : undefined;
209
+
192
210
  return {
193
- content: choice?.message?.content ?? null,
211
+ content: (choice?.message?.content as string | null) ?? null,
194
212
  finishReason: choice?.finish_reason ?? null,
213
+ rawToolCalls,
214
+ assistantMessage,
195
215
  };
196
216
  }
197
217
 
@@ -10,7 +10,7 @@ import {
10
10
  type ExtractionOptions,
11
11
  } from "./human-extraction.js";
12
12
  import { queuePersonaTopicScan, type PersonaTopicContext } from "./persona-topics.js";
13
- import { buildPersonaExpirePrompt, buildPersonaExplorePrompt, buildDescriptionCheckPrompt } from "../../prompts/ceremony/index.js";
13
+ import { buildPersonaExpirePrompt, buildPersonaExplorePrompt, buildDescriptionCheckPrompt, buildRewriteScanPrompt, type RewriteItemType } from "../../prompts/ceremony/index.js";
14
14
 
15
15
  export function isNewDay(lastCeremony: string | undefined, now: Date): boolean {
16
16
  if (!lastCeremony) return true;
@@ -227,6 +227,10 @@ export function handleCeremonyProgress(state: StateManager): void {
227
227
 
228
228
  // Dedup phase: log near-duplicate human entities (dry-run only, no mutations)
229
229
  runDedupPhase(state);
230
+
231
+ // Rewrite phase: fire-and-forget scans for bloated human data items
232
+ // No ceremony_progress gating — Expire/Explore only touch persona topics, zero overlap
233
+ queueRewritePhase(state);
230
234
 
231
235
  // Expire phase: queue LLM calls for each active persona
232
236
  // handlePersonaExpire already chains to Explore → DescriptionCheck
@@ -574,3 +578,78 @@ export function runDedupPhase(state: StateManager): void {
574
578
 
575
579
  console.log(`[ceremony:dedup] Done. ${totalCandidates} total candidate pair(s) found.`);
576
580
  }
581
+
582
+ // =============================================================================
583
+ // REWRITE PHASE (fire-and-forget — queues Low-priority Phase 1 scans)
584
+ // =============================================================================
585
+
586
+ const REWRITE_DESCRIPTION_THRESHOLD = 750;
587
+
588
+ /**
589
+ * Queue Phase 1 "scan" for every human data item whose description exceeds the
590
+ * threshold. Gated on rewrite_model being set in HumanSettings.
591
+ *
592
+ * Fire-and-forget: no ceremony_progress, no blocking. Expire/Explore proceed
593
+ * immediately since they only touch persona topics (zero overlap with human data).
594
+ * Phase 2 items enqueue at Normal priority, naturally processing before more
595
+ * Low-priority Phase 1 scans.
596
+ */
597
+ export function queueRewritePhase(state: StateManager): void {
598
+ const human = state.getHuman();
599
+ const rewriteModel = human.settings?.rewrite_model;
600
+
601
+ if (!rewriteModel) {
602
+ console.log("[ceremony:rewrite] rewrite_model not set — skipping rewrite phase");
603
+ return;
604
+ }
605
+
606
+ const itemsToScan: Array<{ item: DataItemBase; type: RewriteItemType }> = [];
607
+
608
+ for (const fact of human.facts) {
609
+ if ((fact.description?.length ?? 0) > REWRITE_DESCRIPTION_THRESHOLD) {
610
+ itemsToScan.push({ item: fact, type: "fact" });
611
+ }
612
+ }
613
+ for (const trait of human.traits) {
614
+ if ((trait.description?.length ?? 0) > REWRITE_DESCRIPTION_THRESHOLD) {
615
+ itemsToScan.push({ item: trait, type: "trait" });
616
+ }
617
+ }
618
+ for (const topic of human.topics) {
619
+ if ((topic.description?.length ?? 0) > REWRITE_DESCRIPTION_THRESHOLD) {
620
+ itemsToScan.push({ item: topic, type: "topic" });
621
+ }
622
+ }
623
+ for (const person of human.people) {
624
+ if ((person.description?.length ?? 0) > REWRITE_DESCRIPTION_THRESHOLD) {
625
+ itemsToScan.push({ item: person, type: "person" });
626
+ }
627
+ }
628
+
629
+ if (itemsToScan.length === 0) {
630
+ console.log("[ceremony:rewrite] No items above threshold — nothing to rewrite");
631
+ return;
632
+ }
633
+
634
+ console.log(`[ceremony:rewrite] Found ${itemsToScan.length} item(s) above ${REWRITE_DESCRIPTION_THRESHOLD} chars — queueing Phase 1 scans`);
635
+
636
+ for (const { item, type } of itemsToScan) {
637
+ const prompt = buildRewriteScanPrompt({ item, itemType: type });
638
+
639
+ state.queue_enqueue({
640
+ type: LLMRequestType.JSON,
641
+ priority: LLMPriority.Low,
642
+ system: prompt.system,
643
+ user: prompt.user,
644
+ next_step: LLMNextStep.HandleRewriteScan,
645
+ model: rewriteModel,
646
+ data: {
647
+ itemId: item.id,
648
+ itemType: type,
649
+ rewriteModel, // pass through so Phase 1 handler can queue Phase 2 with the same model
650
+ },
651
+ });
652
+ }
653
+
654
+ console.log(`[ceremony:rewrite] Queued ${itemsToScan.length} Phase 1 scan(s) at Low priority`);
655
+ }
@@ -281,11 +281,14 @@ export async function queueItemMatch(
281
281
  break;
282
282
  }
283
283
 
284
+ // Traits are personality patterns — they must only match against other traits.
285
+ // Non-trait candidates (facts, topics, people) must never absorb trait content,
286
+ // and trait candidates must never cross-match into facts/topics/people.
284
287
  const allItemsWithEmbeddings = [
285
- ...human.facts.map(f => ({ ...f, data_type: "fact" as DataItemType })),
288
+ ...(dataType !== "trait" ? human.facts.map(f => ({ ...f, data_type: "fact" as DataItemType })) : []),
286
289
  ...human.traits.map(t => ({ ...t, data_type: "trait" as DataItemType })),
287
- ...human.topics.map(t => ({ ...t, data_type: "topic" as DataItemType })),
288
- ...human.people.map(p => ({ ...p, data_type: "person" as DataItemType })),
290
+ ...(dataType !== "trait" ? human.topics.map(t => ({ ...t, data_type: "topic" as DataItemType })) : []),
291
+ ...(dataType !== "trait" ? human.people.map(p => ({ ...p, data_type: "person" as DataItemType })) : []),
289
292
  ].filter(item => item.embedding && item.embedding.length > 0);
290
293
 
291
294
  let topKItems: Array<{
@@ -321,15 +324,18 @@ export async function queueItemMatch(
321
324
  }
322
325
 
323
326
  if (topKItems.length === 0) {
324
- console.log(`[queueItemMatch] No embeddings available, using all ${human.facts.length + human.traits.length + human.topics.length + human.people.length} items`);
325
-
326
- for (const fact of human.facts) {
327
- topKItems.push({
328
- data_type: "fact",
329
- data_id: fact.id,
330
- data_name: fact.name,
331
- data_description: dataType === "fact" ? fact.description : truncateDescription(fact.description),
332
- });
327
+
328
+ console.log(`[queueItemMatch] No embeddings available, using filtered items (dataType=${dataType})`);
329
+
330
+ if (dataType !== "trait") {
331
+ for (const fact of human.facts) {
332
+ topKItems.push({
333
+ data_type: "fact",
334
+ data_id: fact.id,
335
+ data_name: fact.name,
336
+ data_description: dataType === "fact" ? fact.description : truncateDescription(fact.description),
337
+ });
338
+ }
333
339
  }
334
340
 
335
341
  for (const trait of human.traits) {
@@ -341,22 +347,24 @@ export async function queueItemMatch(
341
347
  });
342
348
  }
343
349
 
344
- for (const topic of human.topics) {
345
- topKItems.push({
346
- data_type: "topic",
347
- data_id: topic.id,
348
- data_name: topic.name,
349
- data_description: dataType === "topic" ? topic.description : truncateDescription(topic.description),
350
- });
351
- }
352
-
353
- for (const person of human.people) {
354
- topKItems.push({
355
- data_type: "person",
356
- data_id: person.id,
357
- data_name: person.name,
358
- data_description: dataType === "person" ? person.description : truncateDescription(person.description),
359
- });
350
+ if (dataType !== "trait") {
351
+ for (const topic of human.topics) {
352
+ topKItems.push({
353
+ data_type: "topic",
354
+ data_id: topic.id,
355
+ data_name: topic.name,
356
+ data_description: dataType === "topic" ? topic.description : truncateDescription(topic.description),
357
+ });
358
+ }
359
+
360
+ for (const person of human.people) {
361
+ topKItems.push({
362
+ data_type: "person",
363
+ data_id: person.id,
364
+ data_name: person.name,
365
+ data_description: dataType === "person" ? person.description : truncateDescription(person.description),
366
+ });
367
+ }
360
368
  }
361
369
  }
362
370
 
@@ -19,6 +19,7 @@ export {
19
19
  queueExplorePhase,
20
20
  queueDescriptionCheck,
21
21
  runHumanCeremony,
22
+ queueRewritePhase,
22
23
  } from "./ceremony.js";
23
24
  export {
24
25
  queuePersonaTopicScan,