ei-tui 1.1.0 → 1.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 (44) hide show
  1. package/package.json +2 -23
  2. package/src/core/handlers/dedup.ts +4 -15
  3. package/src/core/handlers/document-segmentation.ts +2 -3
  4. package/src/core/handlers/heartbeat.ts +5 -10
  5. package/src/core/handlers/human-matching.ts +8 -0
  6. package/src/core/handlers/index.ts +2 -0
  7. package/src/core/handlers/knowledge-synthesis.ts +50 -0
  8. package/src/core/handlers/persona-generation.ts +4 -8
  9. package/src/core/handlers/persona-response.ts +3 -4
  10. package/src/core/handlers/persona-topics.ts +2 -4
  11. package/src/core/handlers/rewrite.ts +26 -9
  12. package/src/core/handlers/rooms.ts +6 -12
  13. package/src/core/llm-client.ts +13 -3
  14. package/src/core/message-manager.ts +2 -4
  15. package/src/core/orchestrators/ceremony.ts +44 -13
  16. package/src/core/orchestrators/human-extraction.ts +10 -1
  17. package/src/core/processor.ts +155 -0
  18. package/src/core/queue-manager.ts +10 -0
  19. package/src/core/state-manager.ts +35 -0
  20. package/src/core/tools/builtin/fetch-memory.ts +6 -6
  21. package/src/core/tools/index.ts +1 -1
  22. package/src/core/tools/types.ts +1 -1
  23. package/src/core/types/data-items.ts +1 -1
  24. package/src/core/types/entities.ts +7 -1
  25. package/src/core/types/enums.ts +1 -0
  26. package/src/core/types/integrations.ts +3 -1
  27. package/src/integrations/claude-code/importer.ts +6 -0
  28. package/src/integrations/cursor/importer.ts +6 -0
  29. package/src/integrations/document/unsource.ts +5 -3
  30. package/src/integrations/opencode/importer.ts +13 -1
  31. package/src/integrations/persona-history/importer.ts +9 -0
  32. package/src/prompts/ceremony/people-rewrite.ts +2 -2
  33. package/src/prompts/ceremony/topic-rewrite.ts +2 -2
  34. package/src/prompts/index.ts +3 -0
  35. package/src/prompts/synthesis/index.ts +101 -0
  36. package/src/prompts/synthesis/types.ts +26 -0
  37. package/tui/src/commands/generate.tsx +98 -0
  38. package/tui/src/commands/unsource.tsx +17 -10
  39. package/tui/src/components/GeneratedDocsOverlay.tsx +136 -0
  40. package/tui/src/components/PromptInput.tsx +2 -0
  41. package/tui/src/context/ei.tsx +49 -2
  42. package/tui/src/util/logger.ts +22 -2
  43. package/tui/src/util/provider-detection.ts +5 -2
  44. package/tui/src/util/yaml-provider.ts +2 -8
@@ -195,6 +195,15 @@ export function queuePersonScan(context: ExtractionContext, state: StateManager,
195
195
 
196
196
  if (chunks.length === 0) return 0;
197
197
 
198
+ // If the persona has a pending_update (reflection in progress), gate person
199
+ // scans so handleHumanPersonScan won't queue updates for persona-linked people.
200
+ // This prevents importers and other callers from bypassing the reflection lock
201
+ // — they don't know about pending_update, so we enforce it here centrally.
202
+ const persona = state.persona_getById(context.personaId);
203
+ const effectiveOptions: ExtractionOptions | undefined = persona?.pending_update
204
+ ? { ...options, reflection_progress: 1 }
205
+ : options;
206
+
198
207
  // Pre-mark messages before enqueuing — prevents duplicate scans if the
199
208
  // queue check fires again during LLM latency (100ms loop × 5s call = 50 dupes)
200
209
  for (const chunk of chunks) {
@@ -225,7 +234,7 @@ export function queuePersonScan(context: ExtractionContext, state: StateManager,
225
234
  user: prompt.user,
226
235
  next_step: LLMNextStep.HandleHumanPersonScan,
227
236
  data: {
228
- ...options,
237
+ ...effectiveOptions,
229
238
  personaId: chunk.personaId,
230
239
  personaDisplayName: chunk.channelDisplayName,
231
240
  analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
@@ -44,6 +44,7 @@ import { EI_WELCOME_MESSAGE, EI_PERSONA_DEFINITION } from "../templates/welcome.
44
44
  import { EMMETT_PERSONA_DEFINITION } from "../templates/emmett.js";
45
45
  import { shouldStartCeremony, startCeremony, handleCeremonyProgress, queueReflectionDrain, queueUserDedupRequest, queueRoomCapture, queuePersonaCapture, checkAndQueueRoomExtraction, queueTargetedPersonUpdate, queueTargetedTopicUpdate } from "./orchestrators/index.js";
46
46
  import { finishDocumentBatch } from "./handlers/document-segmentation.js";
47
+ import { buildSynthesisPrompt } from "../prompts/synthesis/index.js";
47
48
  import { BUILT_IN_FACTS } from "./constants/built-in-facts.js";
48
49
  import { DEFAULT_SEED_TRAITS } from "./constants/seed-traits.js";
49
50
 
@@ -335,6 +336,140 @@ export class Processor {
335
336
  return executeUnsource(preview, this.stateManager);
336
337
  }
337
338
 
339
+ async generateDocument(subject: string): Promise<{ slug: string }> {
340
+ this.bootstrapEmmett();
341
+ const slugBase = subject
342
+ .toLowerCase()
343
+ .replace(/[^a-z0-9]+/g, "-")
344
+ .slice(0, 40);
345
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
346
+ const slug = `${slugBase}_${timestamp}`;
347
+
348
+ const primary = await this.searchHumanData(subject, { limit: 20 });
349
+ if (
350
+ primary.facts.length === 0 &&
351
+ primary.topics.length === 0 &&
352
+ primary.people.length === 0 &&
353
+ primary.quotes.length === 0
354
+ ) {
355
+ throw new Error(`No knowledge found about '${subject}'`);
356
+ }
357
+
358
+ const seenQuoteIds = new Set<string>();
359
+ const seenItemIds = new Set<string>(
360
+ [...primary.topics, ...primary.people, ...primary.facts].map(i => i.id)
361
+ );
362
+
363
+ const MAX_QUOTES_PER_ENTITY = 3;
364
+
365
+ const enrichTopic = (topic: import("../prompts/synthesis/types.js").EnrichedTopic["topic"]) => {
366
+ const linked = this.stateManager.human_quote_getForDataItem(topic.id)
367
+ .sort((a, b) => b.timestamp.localeCompare(a.timestamp))
368
+ .slice(0, MAX_QUOTES_PER_ENTITY);
369
+ linked.forEach(q => seenQuoteIds.add(q.id));
370
+ return { topic, quotes: linked };
371
+ };
372
+
373
+ const enrichPerson = (person: import("../prompts/synthesis/types.js").EnrichedPerson["person"]) => {
374
+ const linked = this.stateManager.human_quote_getForDataItem(person.id)
375
+ .sort((a, b) => b.timestamp.localeCompare(a.timestamp))
376
+ .slice(0, MAX_QUOTES_PER_ENTITY);
377
+ linked.forEach(q => seenQuoteIds.add(q.id));
378
+ return { person, quotes: linked };
379
+ };
380
+
381
+ const enrichedTopics = primary.topics.map(enrichTopic);
382
+ const enrichedPeople = primary.people.map(enrichPerson);
383
+
384
+ const human = this.stateManager.getHuman();
385
+ const allItems = [...human.facts, ...human.topics, ...human.people];
386
+
387
+ const MAX_SECONDARY_ENTITIES = 10;
388
+
389
+ const secondaryTopics: typeof enrichedTopics = [];
390
+ const secondaryPeople: typeof enrichedPeople = [];
391
+ const secondaryFacts: typeof primary.facts = [];
392
+
393
+ outer: for (const quote of [...enrichedTopics.flatMap(e => e.quotes), ...enrichedPeople.flatMap(e => e.quotes)]) {
394
+ for (const itemId of quote.data_item_ids) {
395
+ if (secondaryTopics.length + secondaryPeople.length + secondaryFacts.length >= MAX_SECONDARY_ENTITIES) break outer;
396
+ if (seenItemIds.has(itemId)) continue;
397
+ seenItemIds.add(itemId);
398
+ const item = allItems.find(i => i.id === itemId);
399
+ if (!item) continue;
400
+ if (human.topics.find(t => t.id === itemId)) {
401
+ secondaryTopics.push(enrichTopic(item as typeof primary.topics[0]));
402
+ } else if (human.people.find(p => p.id === itemId)) {
403
+ secondaryPeople.push(enrichPerson(item as typeof primary.people[0]));
404
+ } else if (human.facts.find(f => f.id === itemId)) {
405
+ secondaryFacts.push(item as typeof primary.facts[0]);
406
+ }
407
+ }
408
+ }
409
+
410
+ const standaloneQuotes = primary.quotes.filter(q => !seenQuoteIds.has(q.id));
411
+
412
+ const allLoadedFacts = [...primary.facts, ...secondaryFacts];
413
+ const allLoadedTopics = [...enrichedTopics, ...secondaryTopics];
414
+ const allLoadedPeople = [...enrichedPeople, ...secondaryPeople];
415
+
416
+ const loadedEntityNames = new Map<string, string>();
417
+ for (const f of allLoadedFacts) loadedEntityNames.set(f.id, f.name);
418
+ for (const { topic } of allLoadedTopics) loadedEntityNames.set(topic.id, topic.name);
419
+ for (const { person } of allLoadedPeople) loadedEntityNames.set(person.id, person.name);
420
+
421
+ const prompt = buildSynthesisPrompt({
422
+ subject,
423
+ facts: allLoadedFacts,
424
+ topics: allLoadedTopics,
425
+ people: allLoadedPeople,
426
+ standaloneQuotes,
427
+ loadedEntityNames,
428
+ });
429
+
430
+ const model = this.stateManager.getHuman().settings?.rewrite_model
431
+ ?? this.stateManager.getHuman().settings?.default_model;
432
+
433
+ this.stateManager.queue_enqueue({
434
+ type: LLMRequestType.Raw,
435
+ priority: LLMPriority.Normal,
436
+ system: prompt.system,
437
+ user: prompt.user,
438
+ next_step: LLMNextStep.HandleKnowledgeSynthesis,
439
+ model,
440
+ data: { slug, subject },
441
+ });
442
+
443
+ return { slug };
444
+ }
445
+
446
+ checkGenerationModel(): { model: string; isRewriteModel: boolean } {
447
+ const settings = this.stateManager.getHuman().settings;
448
+ if (settings?.rewrite_model) {
449
+ return { model: settings.rewrite_model, isRewriteModel: true };
450
+ }
451
+ return { model: settings?.default_model ?? "unknown", isRewriteModel: false };
452
+ }
453
+
454
+ async getGeneratedDocumentContent(slug: string): Promise<string | null> {
455
+ const messages = this.stateManager.messages_get("emmet");
456
+ const target = `generate:document:${slug}`;
457
+ const message = messages.find(m => m.source_tag === target);
458
+ return message?.content ?? null;
459
+ }
460
+
461
+ async reRunDocument(slug: string): Promise<{ slug: string }> {
462
+ const docs = this.stateManager.getHuman().settings?.document?.processed_documents ?? {};
463
+ const entry = docs[slug];
464
+ if (!entry || entry.type !== "generated" || !entry.subject) {
465
+ throw new Error(`No generated document found for slug "${slug}"`);
466
+ }
467
+ const subject = entry.subject;
468
+ const preview = this.getUnsourcePreview(`generate:document:${slug}`);
469
+ await this.executeUnsource(preview);
470
+ return this.generateDocument(subject);
471
+ }
472
+
338
473
  /**
339
474
  * Seed built-in tool providers and tools if they don't exist yet.
340
475
  * Called on every startup (after state load/restore) — safe to call repeatedly.
@@ -1087,6 +1222,7 @@ const toolNextSteps = new Set([
1087
1222
  LLMNextStep.HandleEiHeartbeat,
1088
1223
  LLMNextStep.HandleToolContinuation,
1089
1224
  LLMNextStep.HandleDedupCurate,
1225
+ LLMNextStep.HandleKnowledgeSynthesis,
1090
1226
  ]);
1091
1227
  const toolPersonaId =
1092
1228
  personaId ??
@@ -1101,9 +1237,17 @@ const toolNextSteps = new Set([
1101
1237
  (request.next_step === LLMNextStep.HandleToolContinuation &&
1102
1238
  request.data.originalNextStep === LLMNextStep.HandleDedupCurate);
1103
1239
 
1240
+ const isSynthesisRequest = request.next_step === LLMNextStep.HandleKnowledgeSynthesis ||
1241
+ (request.next_step === LLMNextStep.HandleToolContinuation &&
1242
+ request.data.originalNextStep === LLMNextStep.HandleKnowledgeSynthesis);
1243
+
1104
1244
  let tools: ToolDefinition[] = [];
1105
1245
  if (isDedupRequest) {
1106
1246
  tools = SYSTEM_TOOLS.filter(t => t.name === "find_memory");
1247
+ } else if (isSynthesisRequest) {
1248
+ tools = SYSTEM_TOOLS.filter(t =>
1249
+ t.name === "find_memory" || t.name === "fetch_memory" || t.name === "fetch_message"
1250
+ );
1107
1251
  } else if (toolNextSteps.has(request.next_step) && toolPersonaId) {
1108
1252
  tools = [...SYSTEM_TOOLS, ...this.stateManager.tools_getForPersona(toolPersonaId, this.isTUI)];
1109
1253
  }
@@ -1760,6 +1904,17 @@ const toolNextSteps = new Set([
1760
1904
  this.interface.onHumanUpdated?.();
1761
1905
  }
1762
1906
  }
1907
+
1908
+ const isSynthesisCompletion =
1909
+ response.request.next_step === LLMNextStep.HandleKnowledgeSynthesis ||
1910
+ (response.request.next_step === LLMNextStep.HandleToolContinuation &&
1911
+ response.request.data.originalNextStep === LLMNextStep.HandleKnowledgeSynthesis);
1912
+ if (isSynthesisCompletion) {
1913
+ const slug = response.request.data.slug as string;
1914
+ const hasContent = slug && this.stateManager.messages_get("emmet")
1915
+ .some(m => m.source_tag === `generate:document:${slug}`);
1916
+ if (hasContent) this.interface.onDocumentGenerated?.(slug);
1917
+ }
1763
1918
  } catch (err) {
1764
1919
  const errorMsg = err instanceof Error ? err.message : String(err);
1765
1920
  const result = this.stateManager.queue_fail(response.request.id, errorMsg);
@@ -51,6 +51,15 @@ export async function getQueueStatus(sm: StateManager): Promise<QueueStatus> {
51
51
  }
52
52
  const extracting_documents = extractingSet.size > 0 ? Array.from(extractingSet) : undefined;
53
53
 
54
+ const generatingSet: string[] = [];
55
+ for (const item of activeItems) {
56
+ if (item.next_step === LLMNextStep.HandleKnowledgeSynthesis) {
57
+ const slug = (item.data as { slug?: string }).slug;
58
+ if (slug) generatingSet.push(slug);
59
+ }
60
+ }
61
+ const generating_documents = generatingSet.length > 0 ? generatingSet : undefined;
62
+
54
63
  return {
55
64
  state: sm.queue_isPaused()
56
65
  ? "paused"
@@ -62,6 +71,7 @@ export async function getQueueStatus(sm: StateManager): Promise<QueueStatus> {
62
71
  embedding_warning: sm.embedding_getWarning() || undefined,
63
72
  pending_documents,
64
73
  extracting_documents,
74
+ generating_documents,
65
75
  };
66
76
  }
67
77
 
@@ -71,6 +71,7 @@ export class StateManager {
71
71
  this.migrateProviderModel();
72
72
  this.migrateThemes();
73
73
  this.migrateFfaParentIds();
74
+ this.migrateDocumentSettings();
74
75
  }
75
76
 
76
77
  /**
@@ -586,6 +587,40 @@ export class StateManager {
586
587
  }
587
588
  }
588
589
 
590
+ private migrateDocumentSettings(): void {
591
+ const human = this.humanState.get();
592
+ const doc = human.settings?.document;
593
+ if (!doc) return;
594
+
595
+ let migrated = false;
596
+
597
+ const existing = doc.processed_documents ?? {};
598
+ for (const [key, value] of Object.entries(existing)) {
599
+ if (typeof value === "string") {
600
+ (existing as Record<string, unknown>)[key] = { created_at: value, type: "imported" };
601
+ migrated = true;
602
+ }
603
+ }
604
+
605
+ const legacy = (doc as Record<string, unknown>).generated_documents as
606
+ | Record<string, { subject: string; created_at: string }>
607
+ | undefined;
608
+ if (legacy) {
609
+ for (const [slug, record] of Object.entries(legacy)) {
610
+ existing[slug] = { created_at: record.created_at, type: "generated", subject: record.subject };
611
+ }
612
+ delete (doc as Record<string, unknown>).generated_documents;
613
+ migrated = true;
614
+ }
615
+
616
+ if (migrated) {
617
+ doc.processed_documents = existing as import("./types/entities.js").DocumentSettings["processed_documents"];
618
+ this.humanState.set(human);
619
+ this.scheduleSave();
620
+ console.log("[StateManager] Migrated document settings to unified processed_documents schema");
621
+ }
622
+ }
623
+
589
624
  getHuman(): HumanEntity {
590
625
  return this.humanState.get();
591
626
  }
@@ -4,20 +4,20 @@ import type { Fact, Topic, Person, Quote, HumanEntity } from "../../types.js";
4
4
  type GetHuman = () => HumanEntity;
5
5
 
6
6
  function cleanFact(f: Fact): Record<string, unknown> {
7
- const { embedding, rewrite_checked, persona_groups, ...rest } = f;
8
- void embedding; void rewrite_checked; void persona_groups;
7
+ const { embedding, persona_groups, ...rest } = f;
8
+ void embedding; void persona_groups;
9
9
  return rest;
10
10
  }
11
11
 
12
12
  function cleanTopic(t: Topic): Record<string, unknown> {
13
- const { embedding, rewrite_checked, persona_groups, last_ei_asked, ...rest } = t;
14
- void embedding; void rewrite_checked; void persona_groups; void last_ei_asked;
13
+ const { embedding, rewrite_length_floor, persona_groups, last_ei_asked, ...rest } = t;
14
+ void embedding; void rewrite_length_floor; void persona_groups; void last_ei_asked;
15
15
  return rest;
16
16
  }
17
17
 
18
18
  function cleanPerson(p: Person): Record<string, unknown> {
19
- const { embedding, rewrite_checked, persona_groups, last_ei_asked, ...rest } = p;
20
- void embedding; void rewrite_checked; void persona_groups; void last_ei_asked;
19
+ const { embedding, rewrite_length_floor, persona_groups, last_ei_asked, ...rest } = p;
20
+ void embedding; void rewrite_length_floor; void persona_groups; void last_ei_asked;
21
21
  return rest;
22
22
  }
23
23
 
@@ -64,7 +64,7 @@ export const SYSTEM_TOOLS: ToolDefinition[] = [
64
64
  builtin: true,
65
65
  enabled: true,
66
66
  created_at: new Date(0).toISOString(),
67
- max_calls_per_interaction: 3,
67
+ max_calls_per_interaction: 10,
68
68
  },
69
69
  {
70
70
  id: "builtin-fetch-message",
@@ -6,7 +6,7 @@
6
6
  /** A single tool call the LLM wants to make (from the API response). */
7
7
  export interface ToolCall {
8
8
  id: string; // call_abc123 — must be echoed back in tool result message
9
- name: string; // snake_case tool name ("web_search", "read_memory")
9
+ name: string; // snake_case tool name ("web_search", "find_memory")
10
10
  arguments: Record<string, unknown>; // Parsed from JSON string in the API response
11
11
  }
12
12
 
@@ -18,7 +18,7 @@ export interface DataItemBase {
18
18
  sources?: string[]; // Namespaced source identifiers — where items were learned from. Format: "provider:id" (e.g., "opencode:ses_abc123", "cursor:composerId"). Grow-only union.
19
19
  persona_groups?: string[];
20
20
  embedding?: number[];
21
- rewrite_checked?: boolean; // True after rewrite scan finds no changes. Cleared automatically when extraction upserts a fresh item.
21
+ rewrite_length_floor?: number; // Set after every rewrite scan: ceil(description.length * 1.1). Item is skipped by ceremony until description grows past this floor. Preserved across extraction upserts only cleared when description exceeds it.
22
22
  }
23
23
 
24
24
  export interface Fact extends DataItemBase {
@@ -20,9 +20,15 @@ export interface OpenCodeSettings {
20
20
  processed_sessions?: Record<string, string>; // sessionId → ISO timestamp of last import
21
21
  }
22
22
 
23
+ export interface DocumentRecord {
24
+ created_at: string;
25
+ type: "imported" | "generated";
26
+ subject?: string;
27
+ }
28
+
23
29
  export interface DocumentSettings {
24
30
  extraction_model?: string;
25
- processed_documents?: Record<string, string>;
31
+ processed_documents?: Record<string, DocumentRecord>;
26
32
  }
27
33
 
28
34
  export interface CeremonyConfig {
@@ -53,6 +53,7 @@ export enum LLMNextStep {
53
53
  HandleTopicValidate = "handleTopicValidate",
54
54
  HandleReflectionCritic = "handleReflectionCritic",
55
55
  HandleDocumentSegmentation = "handleDocumentSegmentation",
56
+ HandleKnowledgeSynthesis = "handleKnowledgeSynthesis",
56
57
  }
57
58
 
58
59
  export enum ProviderType {
@@ -29,7 +29,7 @@ export interface ToolProvider {
29
29
  export interface ToolDefinition {
30
30
  id: string; // UUID
31
31
  provider_id: string; // FK → ToolProvider.id (required)
32
- name: string; // Snake_case machine name ("web_search", "read_memory")
32
+ name: string; // Snake_case machine name ("web_search", "find_memory")
33
33
  display_name: string; // Human label
34
34
  description: string; // What the LLM reads to decide whether to call this tool
35
35
  input_schema: Record<string, unknown>; // JSON Schema for parameters the LLM can pass
@@ -77,6 +77,7 @@ export interface QueueStatus {
77
77
  embedding_warning?: boolean;
78
78
  pending_documents?: Array<{ batchId: string; filename: string; count: number }>;
79
79
  extracting_documents?: string[];
80
+ generating_documents?: string[];
80
81
  }
81
82
 
82
83
  export interface EiError {
@@ -121,6 +122,7 @@ export interface Ei_Interface {
121
122
  onRoomMessageAdded?: (roomId: string) => void;
122
123
  onRoomMessageQueued?: (roomId: string) => void;
123
124
  onRoomMessageProcessing?: (roomId: string) => void;
125
+ onDocumentGenerated?: (slug: string) => void;
124
126
  }
125
127
 
126
128
  // =============================================================================
@@ -11,6 +11,10 @@ import {
11
11
  queueAllScans,
12
12
  type ExtractionContext,
13
13
  } from "../../core/orchestrators/human-extraction.js";
14
+ import {
15
+ queuePersonRewritePhase,
16
+ queueTopicRewritePhase,
17
+ } from "../../core/orchestrators/ceremony.js";
14
18
  import { isProcessRunning } from "../process-check.js";
15
19
  import { getMachineId } from "../machine-id.js";
16
20
 
@@ -268,6 +272,8 @@ export async function importClaudeCodeSessions(
268
272
  sources: [`claudecode:${getMachineId()}:${targetSession.id}`],
269
273
  };
270
274
 
275
+ queuePersonRewritePhase(stateManager);
276
+ queueTopicRewritePhase(stateManager);
271
277
  const ccSettings = stateManager.getHuman().settings?.claudeCode;
272
278
  queueAllScans(context, stateManager, {
273
279
  extraction_model: ccSettings?.extraction_model,
@@ -13,6 +13,10 @@ import {
13
13
  queueAllScans,
14
14
  type ExtractionContext,
15
15
  } from "../../core/orchestrators/human-extraction.js";
16
+ import {
17
+ queuePersonRewritePhase,
18
+ queueTopicRewritePhase,
19
+ } from "../../core/orchestrators/ceremony.js";
16
20
 
17
21
  export interface CursorImportResult {
18
22
  sessionsProcessed: number;
@@ -227,6 +231,8 @@ export async function importCursorSessions(
227
231
  sources: [`cursor:${getMachineId()}:${targetSession.id}`],
228
232
  };
229
233
 
234
+ queuePersonRewritePhase(stateManager);
235
+ queueTopicRewritePhase(stateManager);
230
236
  queueAllScans(context, stateManager, { external_filter: "only" });
231
237
  result.extractionScansQueued += 4;
232
238
  }
@@ -150,13 +150,15 @@ export async function executeUnsource(
150
150
  stateManager.messages_remove("emmet", sourceMessageIds);
151
151
  }
152
152
 
153
- const filename = preview.sourceTag.startsWith("import:document:")
153
+ const key = preview.sourceTag.startsWith("import:document:")
154
154
  ? preview.sourceTag.slice("import:document:".length)
155
- : preview.sourceTag;
155
+ : preview.sourceTag.startsWith("generate:document:")
156
+ ? preview.sourceTag.slice("generate:document:".length)
157
+ : preview.sourceTag;
156
158
 
157
159
  const human = stateManager.getHuman();
158
160
  if (human.settings?.document?.processed_documents) {
159
- delete human.settings.document.processed_documents[filename];
161
+ delete human.settings.document.processed_documents[key];
160
162
  stateManager.setHuman(human);
161
163
  }
162
164
 
@@ -8,6 +8,10 @@ import {
8
8
  queueAllScans,
9
9
  type ExtractionContext,
10
10
  } from "../../core/orchestrators/human-extraction.js";
11
+ import {
12
+ queuePersonRewritePhase,
13
+ queueTopicRewritePhase,
14
+ } from "../../core/orchestrators/ceremony.js";
11
15
  import { isProcessRunning } from "../process-check.js";
12
16
  import { getMachineId } from "../machine-id.js";
13
17
 
@@ -195,6 +199,7 @@ export async function importOpenCodeSessions(
195
199
 
196
200
  const cutoffIso = processedSessions[targetSession.id] ?? null;
197
201
  const cutoffMs = cutoffIso ? new Date(cutoffIso).getTime() : null;
202
+ let anyPersonaHasChanges = false;
198
203
 
199
204
  for (const [, { persona, msgs: agentMsgs, isNew, agentName }] of byPersonaId) {
200
205
  if (isNew) {
@@ -252,6 +257,7 @@ export async function importOpenCodeSessions(
252
257
  };
253
258
 
254
259
  if (!signal?.aborted) {
260
+ anyPersonaHasChanges = true;
255
261
  const openCodeSettings = stateManager.getHuman().settings?.opencode;
256
262
  queueAllScans(context, stateManager, {
257
263
  extraction_model: openCodeSettings?.extraction_model,
@@ -264,7 +270,13 @@ export async function importOpenCodeSessions(
264
270
 
265
271
  result.sessionsProcessed = 1;
266
272
 
267
- // ─── Step 6: Advance extraction state ────────────────────────────────
273
+ // ─── Step 6: Queue rewrite checks if any persona had new messages ─────
274
+ if (anyPersonaHasChanges && !signal?.aborted) {
275
+ queuePersonRewritePhase(stateManager);
276
+ queueTopicRewritePhase(stateManager);
277
+ }
278
+
279
+ // ─── Step 7: Advance extraction state ────────────────────────────────
268
280
  updateExtractionState(stateManager, targetSession);
269
281
 
270
282
  console.log(
@@ -6,6 +6,10 @@ import {
6
6
  queueFactFind,
7
7
  type ExtractionContext,
8
8
  } from "../../core/orchestrators/human-extraction.js";
9
+ import {
10
+ queuePersonRewritePhase,
11
+ queueTopicRewritePhase,
12
+ } from "../../core/orchestrators/ceremony.js";
9
13
 
10
14
  export interface PersonaHistoryImportResult {
11
15
  daysQueued: number;
@@ -141,6 +145,11 @@ export async function importPersonaHistory(
141
145
 
142
146
  result.daysQueued = 1;
143
147
 
148
+ if (result.scansQueued > 0) {
149
+ queuePersonRewritePhase(stateManager);
150
+ queueTopicRewritePhase(stateManager);
151
+ }
152
+
144
153
  const isLastDay = currentDate >= today;
145
154
  advanceProgress(stateManager, currentDate, isLastDay);
146
155
 
@@ -45,7 +45,7 @@ Rules:
45
45
  - Be specific: "React performance patterns" beats "technical stuff"
46
46
  - If the record is clean — everything in it passes the test — return an empty array
47
47
 
48
- Return a raw JSON array of strings. No markdown fencing, no commentary.
48
+ Return a raw JSON array of strings. No markdown fencing, no commentary. Thinking text WILL break the parser.
49
49
 
50
50
  Example — a Person named "Nicholas" whose description includes sprint ticket numbers:
51
51
  ["CMIDP sprint ticket assignments", "ASU Data Lake access provisioning details"]`;
@@ -60,7 +60,7 @@ Example — a Person named "Nicholas" whose description includes sprint ticket n
60
60
 
61
61
  ---
62
62
 
63
- Return a raw JSON array of subject phrases found in this Person record that don't belong there. Return [] if the record is clean.`;
63
+ Return a raw JSON array of subject phrases found in this Person record that don't belong there. Return [] if the record is clean. Thinking text WILL break the parser.`;
64
64
 
65
65
  return { system, user };
66
66
  }
@@ -35,7 +35,7 @@ Rules:
35
35
  - Be specific: "TypeScript coding conventions" beats "technical preferences"
36
36
  - If the record is cohesive and on-topic despite its length, return an empty array
37
37
  ${technicalGuidance}
38
- Return a raw JSON array of strings. No markdown fencing, no commentary.
38
+ Return a raw JSON array of strings. No markdown fencing, no commentary. Thinking text WILL break the parser.
39
39
 
40
40
  Example — a Topic named "Software Engineering" whose description also discusses vim keybindings, git conventions, and AI tooling:
41
41
  ["vim keybindings and editor configuration", "git and GitHub workflow conventions", "AI coding assistant preferences"]`;
@@ -51,7 +51,7 @@ Example — a Topic named "Software Engineering" whose description also discusse
51
51
  ]
52
52
  \`\`\`
53
53
 
54
- Respond with raw JSON array only.`;
54
+ Respond with raw JSON array only. Thinking text WILL break the parser.`;
55
55
 
56
56
  const user = `${payload}
57
57
 
@@ -79,3 +79,6 @@ export type {
79
79
  RoomHistoryMessage,
80
80
  RoomJudgeCandidate,
81
81
  } from "./room/types.js";
82
+
83
+ export { buildSynthesisPrompt } from "./synthesis/index.js";
84
+ export type { SynthesisPromptData } from "./synthesis/types.js";