ei-tui 0.9.3 → 1.0.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 (94) hide show
  1. package/README.md +22 -3
  2. package/package.json +8 -1
  3. package/src/README.md +10 -26
  4. package/src/core/context-utils.ts +2 -2
  5. package/src/core/handlers/document-segmentation.ts +113 -0
  6. package/src/core/handlers/heartbeat.ts +9 -1
  7. package/src/core/handlers/human-extraction.ts +4 -1
  8. package/src/core/handlers/human-matching.ts +5 -53
  9. package/src/core/handlers/index.ts +3 -51
  10. package/src/core/handlers/persona-generation.ts +1 -28
  11. package/src/core/handlers/rewrite.ts +13 -9
  12. package/src/core/handlers/utils.ts +2 -9
  13. package/src/core/heartbeat-manager.ts +5 -5
  14. package/src/core/llm-client.ts +11 -1
  15. package/src/core/message-manager.ts +26 -23
  16. package/src/core/orchestrators/ceremony.ts +87 -49
  17. package/src/core/orchestrators/extraction-chunker.ts +3 -3
  18. package/src/core/orchestrators/human-extraction.ts +22 -18
  19. package/src/core/orchestrators/index.ts +0 -1
  20. package/src/core/orchestrators/persona-topics.ts +1 -1
  21. package/src/core/orchestrators/room-extraction.ts +5 -5
  22. package/src/core/persona-manager.ts +4 -0
  23. package/src/core/processor.ts +98 -22
  24. package/src/core/prompt-context-builder.ts +7 -6
  25. package/src/core/queue-manager.ts +35 -0
  26. package/src/core/state/personas.ts +1 -17
  27. package/src/core/state/queue.ts +9 -1
  28. package/src/core/state-manager.ts +4 -66
  29. package/src/core/types/entities.ts +17 -3
  30. package/src/core/types/enums.ts +1 -2
  31. package/src/core/types/integrations.ts +2 -0
  32. package/src/core/types/llm.ts +9 -0
  33. package/src/core/types/rooms.ts +1 -1
  34. package/src/integrations/claude-code/importer.ts +1 -1
  35. package/src/integrations/cursor/importer.ts +1 -1
  36. package/src/integrations/document/chunker.ts +88 -0
  37. package/src/integrations/document/importer.ts +82 -0
  38. package/src/integrations/document/index.ts +2 -0
  39. package/src/integrations/document/invoice.ts +63 -0
  40. package/src/integrations/document/types.ts +16 -0
  41. package/src/integrations/document/unsource.ts +164 -0
  42. package/src/integrations/opencode/importer.ts +1 -1
  43. package/src/integrations/persona-history/importer.ts +197 -0
  44. package/src/integrations/persona-history/index.ts +3 -0
  45. package/src/integrations/persona-history/types.ts +7 -0
  46. package/src/prompts/ceremony/dedup.ts +7 -3
  47. package/src/prompts/ceremony/index.ts +2 -11
  48. package/src/prompts/ceremony/people-rewrite.ts +190 -0
  49. package/src/prompts/ceremony/{rewrite.ts → topic-rewrite.ts} +103 -78
  50. package/src/prompts/ceremony/types.ts +1 -42
  51. package/src/prompts/generation/index.ts +0 -3
  52. package/src/prompts/generation/types.ts +0 -15
  53. package/src/prompts/heartbeat/check.ts +18 -6
  54. package/src/prompts/heartbeat/types.ts +2 -1
  55. package/src/prompts/human/index.ts +0 -2
  56. package/src/prompts/human/person-scan.ts +13 -4
  57. package/src/prompts/human/topic-scan.ts +16 -2
  58. package/src/prompts/human/topic-update.ts +36 -4
  59. package/src/prompts/human/types.ts +1 -16
  60. package/src/prompts/index.ts +0 -19
  61. package/src/prompts/reflection/index.ts +35 -5
  62. package/src/prompts/reflection/types.ts +1 -1
  63. package/src/prompts/response/index.ts +5 -0
  64. package/src/prompts/response/sections.ts +26 -0
  65. package/src/prompts/response/types.ts +3 -0
  66. package/src/storage/indexed.ts +4 -0
  67. package/src/storage/interface.ts +1 -0
  68. package/src/storage/local.ts +4 -0
  69. package/src/templates/emmett.ts +49 -0
  70. package/tui/README.md +22 -0
  71. package/tui/src/app.tsx +9 -6
  72. package/tui/src/commands/delete.tsx +7 -1
  73. package/tui/src/commands/import.tsx +30 -0
  74. package/tui/src/commands/registry.test.ts +10 -5
  75. package/tui/src/commands/unsource.tsx +115 -0
  76. package/tui/src/components/PromptInput.tsx +4 -0
  77. package/tui/src/components/WelcomeOverlay.tsx +58 -32
  78. package/tui/src/context/ei.tsx +80 -60
  79. package/tui/src/globals.d.ts +57 -0
  80. package/tui/src/index.tsx +14 -0
  81. package/tui/src/storage/file.ts +11 -5
  82. package/tui/src/util/e2e-flags.ts +4 -3
  83. package/tui/src/util/help-content.ts +20 -0
  84. package/tui/src/util/provider-detection.ts +251 -0
  85. package/tui/src/util/yaml-human.ts +7 -1
  86. package/tui/src/util/yaml-persona.ts +8 -4
  87. package/tui/src/util/yaml-settings.ts +3 -3
  88. package/src/core/orchestrators/person-migration.ts +0 -55
  89. package/src/prompts/ceremony/description-check.ts +0 -54
  90. package/src/prompts/ceremony/expire.ts +0 -37
  91. package/src/prompts/ceremony/explore.ts +0 -77
  92. package/src/prompts/ceremony/person-migration.ts +0 -77
  93. package/src/prompts/generation/descriptions.ts +0 -91
  94. package/src/prompts/human/fact-scan.ts +0 -150
@@ -274,7 +274,17 @@ export async function callLLMRaw(
274
274
  };
275
275
 
276
276
  if (modelConfig?.thinking_budget !== undefined) {
277
- requestBody.think = { budget_tokens: modelConfig.thinking_budget };
277
+ if (modelConfig.thinking_budget === 0) {
278
+ // Universal kill switch — works on Ollama, LM Studio, and all OpenAI-compat providers.
279
+ requestBody.reasoning_effort = "none";
280
+ } else {
281
+ // Pass both signals: providers that honor the token budget get it (Qwen3 via Ollama,
282
+ // Anthropic), providers that reduce thinking to on/off use reasoning_effort as the
283
+ // on-signal (Gemma4 via Ollama/LM Studio). Non-conflicting — each provider reads
284
+ // whichever field it understands.
285
+ requestBody.reasoning_effort = "high";
286
+ requestBody.think = { budget_tokens: modelConfig.thinking_budget };
287
+ }
278
288
  }
279
289
 
280
290
  if (options.tools && options.tools.length > 0) {
@@ -177,25 +177,27 @@ export async function sendMessage(
177
177
 
178
178
  const history = sm.messages_get(persona.id);
179
179
 
180
- const traitExtractionData: PersonaTraitExtractionPromptData = {
181
- persona_name: persona.display_name,
182
- current_traits: persona.traits,
183
- messages_context: history.slice(-11, -1),
184
- messages_analyze: [message],
185
- };
186
- const traitPrompt = buildPersonaTraitExtractionPrompt(traitExtractionData);
187
-
188
- sm.queue_enqueue({
189
- type: LLMRequestType.JSON,
190
- priority: LLMPriority.Low,
191
- system: traitPrompt.system,
192
- user: traitPrompt.user,
193
- next_step: LLMNextStep.HandlePersonaTraitExtraction,
194
- model: getModelForPersona(persona.id),
195
- data: { personaId: persona.id, personaDisplayName: persona.display_name },
196
- });
180
+ if (!persona.is_static) {
181
+ const traitExtractionData: PersonaTraitExtractionPromptData = {
182
+ persona_name: persona.display_name,
183
+ current_traits: persona.traits,
184
+ messages_context: history.slice(-11, -1),
185
+ messages_analyze: [message],
186
+ };
187
+ const traitPrompt = buildPersonaTraitExtractionPrompt(traitExtractionData);
188
+
189
+ sm.queue_enqueue({
190
+ type: LLMRequestType.JSON,
191
+ priority: LLMPriority.Low,
192
+ system: traitPrompt.system,
193
+ user: traitPrompt.user,
194
+ next_step: LLMNextStep.HandlePersonaTraitExtraction,
195
+ model: getModelForPersona(persona.id),
196
+ data: { personaId: persona.id, personaDisplayName: persona.display_name },
197
+ });
197
198
 
198
- checkAndQueueHumanExtraction(sm, persona.id, persona.display_name, history);
199
+ checkAndQueueHumanExtraction(sm, persona.id, persona.display_name, history);
200
+ }
199
201
  }
200
202
 
201
203
  // =============================================================================
@@ -223,7 +225,7 @@ export function checkAndQueueHumanExtraction(
223
225
  if (unextractedFacts.length > 0 && unextractedFacts.length >= factsThreshold) {
224
226
  const context: ExtractionContext = {
225
227
  personaId,
226
- personaDisplayName,
228
+ channelDisplayName: personaDisplayName,
227
229
  messages_context: history.filter((m) => m.f === true),
228
230
  messages_analyze: unextractedFacts,
229
231
  extraction_flag: "f",
@@ -239,7 +241,7 @@ export function checkAndQueueHumanExtraction(
239
241
  if (unextractedTopics.length > 0 && unextractedTopics.length >= topicsThreshold) {
240
242
  const context: ExtractionContext = {
241
243
  personaId,
242
- personaDisplayName,
244
+ channelDisplayName: personaDisplayName,
243
245
  messages_context: history.filter((m) => m.t === true),
244
246
  messages_analyze: unextractedTopics,
245
247
  extraction_flag: "t",
@@ -257,7 +259,7 @@ export function checkAndQueueHumanExtraction(
257
259
  const personScanOptions = personaForScan?.pending_update ? { reflection_progress: 1 } : undefined;
258
260
  const context: ExtractionContext = {
259
261
  personaId,
260
- personaDisplayName,
262
+ channelDisplayName: personaDisplayName,
261
263
  messages_context: history.filter((m) => m.p === true),
262
264
  messages_analyze: unextractedPeople,
263
265
  extraction_flag: "p",
@@ -282,8 +284,9 @@ export function fetchMessagesForLLM(
282
284
 
283
285
  const human = sm.getHuman();
284
286
  const history = sm.messages_get(personaId);
285
- const contextWindowHours = persona.context_window_hours ?? human.settings?.default_context_window_hours ?? 8;
286
- const filteredHistory = filterMessagesForContext(history, persona.context_boundary, contextWindowHours);
287
+ const contextWindowMs = persona.context_window_ms ?? human.settings?.default_context_window_ms ?? 28800000;
288
+ const MAX_RESPONSE_MESSAGES = 50;
289
+ const filteredHistory = filterMessagesForContext(history, persona.context_boundary, contextWindowMs).slice(-MAX_RESPONSE_MESSAGES);
287
290
 
288
291
  const humanName = human.settings?.name_display
289
292
  || human.facts?.find(f => f.name === "Nickname/Preferred Name")?.description
@@ -1,4 +1,4 @@
1
- import { LLMRequestType, LLMPriority, LLMNextStep, RoomMode, ContextStatus, type CeremonyConfig, type PersonaTopic, type Topic, type DataItemBase } from "../types.js";
1
+ import { LLMRequestType, LLMPriority, LLMNextStep, RoomMode, ContextStatus, type CeremonyConfig, type PersonaTopic, type Topic } from "../types.js";
2
2
  import type { StateManager } from "../state-manager.js";
3
3
  import { normalizeRoomMessages } from "../handlers/utils.js";
4
4
  import { applyDecayToValue } from "../utils/index.js";
@@ -12,8 +12,9 @@ import {
12
12
  } from "./human-extraction.js";
13
13
  import { queuePersonaTopicRating, type PersonaTopicContext, type PersonaTopicOptions } from "./persona-topics.js";
14
14
  import { getRoomVisibleMessages, queueRoomHumanExtraction } from "./room-extraction.js";
15
- import { queuePersonMigration } from "./person-migration.js";
16
- import { buildRewriteScanPrompt, type RewriteItemType } from "../../prompts/ceremony/index.js";
15
+ import { type RewriteItemType } from "../../prompts/ceremony/index.js";
16
+ import { buildPersonRewriteScanPrompt } from "../../prompts/ceremony/people-rewrite.js";
17
+ import { buildTopicRewriteScanPrompt } from "../../prompts/ceremony/topic-rewrite.js";
17
18
  import { buildReflectionCriticPrompt } from "../../prompts/reflection/index.js";
18
19
  import { getModelForPersona } from "../heartbeat-manager.js";
19
20
 
@@ -52,7 +53,7 @@ export function shouldStartCeremony(config: CeremonyConfig, state: StateManager,
52
53
  * Start the ceremony by queuing Exposure scans for all active personas with recent activity.
53
54
  *
54
55
  * IMPORTANT: Sets last_ceremony FIRST to prevent re-triggering from the processor loop.
55
- * The actual Decay → PruneExpire Explore phases happen later via handleCeremonyProgress
56
+ * The actual Decay → Person Rewrite Topic Rewrite phases happen later via handleCeremonyProgress
56
57
  * once all exposure scans have completed.
57
58
  */
58
59
  export function startCeremony(state: StateManager): void {
@@ -76,10 +77,6 @@ export function startCeremony(state: StateManager): void {
76
77
  },
77
78
  });
78
79
 
79
- // PHASE 1: Person Migration
80
- console.log("[ceremony] Starting Phase 1: Person Migration");
81
- queuePersonMigration(state);
82
-
83
80
  // Check if migration work was queued
84
81
  if (!state.queue_hasPendingCeremonies()) {
85
82
  // No migration work found → immediately advance to Expose phase
@@ -110,7 +107,7 @@ function queueExposurePhase(personaId: string, state: StateManager, options?: Ex
110
107
  if (unextractedFacts.length > 0) {
111
108
  const context: ExtractionContext = {
112
109
  personaId,
113
- personaDisplayName: persona.display_name,
110
+ channelDisplayName: persona.display_name,
114
111
  messages_context: allMessages.filter(m => m.f === true),
115
112
  messages_analyze: unextractedFacts,
116
113
  extraction_flag: "f",
@@ -123,7 +120,7 @@ function queueExposurePhase(personaId: string, state: StateManager, options?: Ex
123
120
  if (unextractedTopics.length > 0) {
124
121
  const context: ExtractionContext = {
125
122
  personaId,
126
- personaDisplayName: persona.display_name,
123
+ channelDisplayName: persona.display_name,
127
124
  messages_context: allMessages.filter(m => m.t === true),
128
125
  messages_analyze: unextractedTopics,
129
126
  extraction_flag: "t",
@@ -135,7 +132,7 @@ function queueExposurePhase(personaId: string, state: StateManager, options?: Ex
135
132
  if (unextractedPeople.length > 0) {
136
133
  const context: ExtractionContext = {
137
134
  personaId,
138
- personaDisplayName: persona.display_name,
135
+ channelDisplayName: persona.display_name,
139
136
  messages_context: allMessages.filter(m => m.p === true),
140
137
  messages_analyze: unextractedPeople,
141
138
  extraction_flag: "p",
@@ -172,7 +169,7 @@ function queueExposurePhase(personaId: string, state: StateManager, options?: Ex
172
169
  * AND at the end of startCeremony (for the zero-messages edge case).
173
170
  *
174
171
  * If any ceremony_progress items remain in the queue, does nothing — more work pending.
175
- * Phase 1: Dedup → Phase 2: Expose → Phase 3: EventSummary → Decay → Expire
172
+ * Phase 1: Dedup → Phase 2: Expose → Phase 3: EventSummary → Decay → Phase 4: Person Rewrite → Topic Rewrite (fire-and-forget)
176
173
  */
177
174
  export function handleCeremonyProgress(state: StateManager, lastPhase: number): void {
178
175
  if (state.queue_hasPendingCeremonies()) {
@@ -241,6 +238,12 @@ export function handleCeremonyProgress(state: StateManager, lastPhase: number):
241
238
  return;
242
239
  }
243
240
 
241
+ if (lastPhase === 4) {
242
+ console.log("[ceremony:progress] Person Rewrite complete, starting Topic Rewrite");
243
+ queueTopicRewritePhase(state);
244
+ return;
245
+ }
246
+
244
247
  if (lastPhase === 2) {
245
248
  console.log("[ceremony:progress] Expose complete, starting EventSummary phase");
246
249
  const options: ExtractionOptions = { ceremony_progress: 3 };
@@ -254,7 +257,7 @@ export function handleCeremonyProgress(state: StateManager, lastPhase: number):
254
257
  return;
255
258
  }
256
259
 
257
- // Phase 3 (EventSummary) complete → advance to Decay/Prune/Expire/Explore
260
+ // Phase 3 (EventSummary) complete → advance to Decay/Prune then Person Rewrite (phase 4)
258
261
  console.log("[ceremony:progress] EventSummary complete, advancing to Decay");
259
262
 
260
263
  const personas = state.persona_getAll();
@@ -281,8 +284,16 @@ export function handleCeremonyProgress(state: StateManager, lastPhase: number):
281
284
  // Human ceremony: decay topics + people
282
285
  runHumanCeremony(state);
283
286
 
284
- // Rewrite phase: fire-and-forget scans for bloated human data items
285
- queueRewritePhase(state);
287
+ // Person Rewrite phase (phase 4): scan bloated Person records, extract Topics from them.
288
+ // Gated via ceremony_progress so Topic Rewrite can run after — Topics created here
289
+ // need to be visible before Topic Rewrite snapshots the threshold.
290
+ queuePersonRewritePhase(state);
291
+
292
+ // Zero-work guard: if no person rewrites queued, advance to topic rewrite immediately
293
+ if (!state.queue_hasPendingCeremonies()) {
294
+ console.log("[ceremony:progress] No person rewrite work, advancing to Topic Rewrite");
295
+ handleCeremonyProgress(state, 4);
296
+ }
286
297
 
287
298
  // Reflection phase: fire-and-forget critic calls for persona person records above threshold
288
299
  queueReflectionPhase(state);
@@ -446,15 +457,6 @@ export function runHumanCeremony(state: StateManager): void {
446
457
 
447
458
  const REWRITE_DESCRIPTION_THRESHOLD = 750;
448
459
 
449
- /**
450
- * Queue Phase 1 "scan" for every human data item whose description exceeds the
451
- * threshold. Gated on rewrite_model being set in HumanSettings.
452
- *
453
- * Fire-and-forget: no ceremony_progress, no blocking. Expire/Explore proceed
454
- * immediately since they only touch persona topics (zero overlap with human data).
455
- * Phase 2 items enqueue at Normal priority, naturally processing before more
456
- * Low-priority Phase 1 scans.
457
- */
458
460
  /**
459
461
  * Forces an unconditional, threshold-bypassing Person scan on Apply/Dismiss.
460
462
  * Cannot be replaced by checkAndQueueHumanExtraction — that function gates on
@@ -475,7 +477,7 @@ export function queueReflectionDrain(personaId: string, state: StateManager): vo
475
477
 
476
478
  const context: ExtractionContext = {
477
479
  personaId,
478
- personaDisplayName: persona.display_name,
480
+ channelDisplayName: persona.display_name,
479
481
  messages_context: allMessages.filter(m => m.p === true),
480
482
  messages_analyze: unextractedPeople,
481
483
  extraction_flag: "p",
@@ -484,41 +486,77 @@ export function queueReflectionDrain(personaId: string, state: StateManager): vo
484
486
  console.log(`[reflection:drain] Queued Person scan for ${persona.display_name} (${unextractedPeople.length} messages) — clears on completion`);
485
487
  }
486
488
 
487
- export function queueRewritePhase(state: StateManager): void {
488
- const human = state.getHuman();
489
- const rewriteModel = human.settings?.rewrite_model;
489
+ function getRewriteModel(state: StateManager): string | undefined {
490
+ return state.getHuman().settings?.rewrite_model;
491
+ }
490
492
 
493
+ export function queuePersonRewritePhase(state: StateManager): void {
494
+ const rewriteModel = getRewriteModel(state);
491
495
  if (!rewriteModel) {
492
- console.log("[ceremony:rewrite] rewrite_model not set — skipping rewrite phase");
496
+ console.log("[ceremony:rewrite] rewrite_model not set — skipping person rewrite phase");
493
497
  return;
494
498
  }
495
499
 
496
- const itemsToScan: Array<{ item: DataItemBase; type: RewriteItemType }> = [];
497
-
498
- for (const topic of human.topics) {
499
- if ((topic.description?.length ?? 0) > REWRITE_DESCRIPTION_THRESHOLD && !topic.rewrite_checked) {
500
- itemsToScan.push({ item: topic, type: "topic" });
501
- }
502
- }
503
- for (const person of human.people) {
500
+ const human = state.getHuman();
501
+ const personsToScan = human.people.filter(person => {
504
502
  const isPersonaLinked = (person.identifiers ?? []).some(
505
503
  i => i.type.toLowerCase() === 'ei persona'
506
504
  );
507
- if (!isPersonaLinked && (person.description?.length ?? 0) > REWRITE_DESCRIPTION_THRESHOLD && !person.rewrite_checked) {
508
- itemsToScan.push({ item: person, type: "person" });
509
- }
505
+ return !isPersonaLinked
506
+ && (person.description?.length ?? 0) > REWRITE_DESCRIPTION_THRESHOLD
507
+ && !person.rewrite_checked;
508
+ });
509
+
510
+ if (personsToScan.length === 0) {
511
+ console.log("[ceremony:rewrite] No persons above threshold — skipping person rewrite phase");
512
+ return;
513
+ }
514
+
515
+ console.log(`[ceremony:rewrite] Found ${personsToScan.length} person(s) above ${REWRITE_DESCRIPTION_THRESHOLD} chars — queueing person rewrite scans`);
516
+
517
+ for (const person of personsToScan) {
518
+ const prompt = buildPersonRewriteScanPrompt({ item: person, itemType: "person" });
519
+ state.queue_enqueue({
520
+ type: LLMRequestType.JSON,
521
+ priority: LLMPriority.Low,
522
+ system: prompt.system,
523
+ user: prompt.user,
524
+ next_step: LLMNextStep.HandleRewriteScan,
525
+ model: rewriteModel,
526
+ data: {
527
+ itemId: person.id,
528
+ itemType: "person" as RewriteItemType,
529
+ rewriteModel,
530
+ ceremony_progress: 4,
531
+ },
532
+ });
510
533
  }
511
534
 
512
- if (itemsToScan.length === 0) {
513
- console.log("[ceremony:rewrite] No items above threshold — nothing to rewrite");
535
+ console.log(`[ceremony:rewrite] Queued ${personsToScan.length} person rewrite scan(s)`);
536
+ }
537
+
538
+ export function queueTopicRewritePhase(state: StateManager): void {
539
+ const rewriteModel = getRewriteModel(state);
540
+ if (!rewriteModel) {
541
+ console.log("[ceremony:rewrite] rewrite_model not set — skipping topic rewrite phase");
514
542
  return;
515
543
  }
516
544
 
517
- console.log(`[ceremony:rewrite] Found ${itemsToScan.length} item(s) above ${REWRITE_DESCRIPTION_THRESHOLD} chars — queueing Phase 1 scans`);
545
+ const human = state.getHuman();
546
+ const topicsToScan = human.topics.filter(topic =>
547
+ (topic.description?.length ?? 0) > REWRITE_DESCRIPTION_THRESHOLD
548
+ && !topic.rewrite_checked
549
+ );
550
+
551
+ if (topicsToScan.length === 0) {
552
+ console.log("[ceremony:rewrite] No topics above threshold — skipping topic rewrite phase");
553
+ return;
554
+ }
518
555
 
519
- for (const { item, type } of itemsToScan) {
520
- const prompt = buildRewriteScanPrompt({ item, itemType: type });
556
+ console.log(`[ceremony:rewrite] Found ${topicsToScan.length} topic(s) above ${REWRITE_DESCRIPTION_THRESHOLD} chars — queueing topic rewrite scans`);
521
557
 
558
+ for (const topic of topicsToScan) {
559
+ const prompt = buildTopicRewriteScanPrompt({ item: topic, itemType: "topic" });
522
560
  state.queue_enqueue({
523
561
  type: LLMRequestType.JSON,
524
562
  priority: LLMPriority.Low,
@@ -527,14 +565,14 @@ export function queueRewritePhase(state: StateManager): void {
527
565
  next_step: LLMNextStep.HandleRewriteScan,
528
566
  model: rewriteModel,
529
567
  data: {
530
- itemId: item.id,
531
- itemType: type,
532
- rewriteModel, // pass through so Phase 1 handler can queue Phase 2 with the same model
568
+ itemId: topic.id,
569
+ itemType: "topic" as RewriteItemType,
570
+ rewriteModel,
533
571
  },
534
572
  });
535
573
  }
536
574
 
537
- console.log(`[ceremony:rewrite] Queued ${itemsToScan.length} Phase 1 scan(s) at Low priority`);
575
+ console.log(`[ceremony:rewrite] Queued ${topicsToScan.length} topic rewrite scan(s)`);
538
576
  }
539
577
 
540
578
  function queueEventSummaryForAll(state: StateManager, options?: ExtractionOptions): void {
@@ -64,7 +64,7 @@ export function chunkExtractionContext(
64
64
  context: ExtractionContext,
65
65
  maxTokens: number = DEFAULT_MAX_TOKENS
66
66
  ): ChunkedContextResult {
67
- const { personaId, personaDisplayName, messages_context, messages_analyze } = context;
67
+ const { personaId, channelDisplayName: personaDisplayName, messages_context, messages_analyze } = context;
68
68
 
69
69
  if (messages_analyze.length === 0) {
70
70
  return {
@@ -85,7 +85,7 @@ export function chunkExtractionContext(
85
85
  return {
86
86
  chunks: [{
87
87
  personaId,
88
- personaDisplayName,
88
+ channelDisplayName: personaDisplayName,
89
89
  messages_context: fittedContext,
90
90
  messages_analyze,
91
91
  }],
@@ -111,7 +111,7 @@ export function chunkExtractionContext(
111
111
 
112
112
  chunks.push({
113
113
  personaId,
114
- personaDisplayName,
114
+ channelDisplayName: personaDisplayName,
115
115
  messages_context: currentContext,
116
116
  messages_analyze: pulled,
117
117
  });
@@ -55,7 +55,7 @@ function buildParticipantContext(personaId: string, state: StateManager): Partic
55
55
 
56
56
  export interface ExtractionContext {
57
57
  personaId: string;
58
- personaDisplayName: string;
58
+ channelDisplayName: string;
59
59
  messages_context: Message[];
60
60
  messages_analyze: Message[];
61
61
  extraction_flag?: "f" | "t" | "p" | "e";
@@ -118,7 +118,7 @@ export function queueFactFind(context: ExtractionContext, state: StateManager, o
118
118
 
119
119
  for (const chunk of chunks) {
120
120
  const prompt = buildFactFindPrompt({
121
- persona_name: chunk.personaDisplayName,
121
+ persona_name: chunk.channelDisplayName,
122
122
  missing_fact_names,
123
123
  messages_context: chunk.messages_context,
124
124
  messages_analyze: chunk.messages_analyze,
@@ -134,7 +134,7 @@ export function queueFactFind(context: ExtractionContext, state: StateManager, o
134
134
  data: {
135
135
  ...options,
136
136
  personaId: chunk.personaId,
137
- personaDisplayName: chunk.personaDisplayName,
137
+ personaDisplayName: chunk.channelDisplayName,
138
138
  analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
139
139
  extraction_flag: context.extraction_flag,
140
140
  message_ids_to_mark: chunk.messages_analyze.map(m => m.id),
@@ -160,10 +160,11 @@ export function queueTopicScan(context: ExtractionContext, state: StateManager,
160
160
 
161
161
  for (const chunk of chunks) {
162
162
  const prompt = buildHumanTopicScanPrompt({
163
- persona_name: chunk.personaDisplayName,
163
+ persona_name: chunk.channelDisplayName,
164
164
  messages_context: chunk.messages_context,
165
165
  messages_analyze: chunk.messages_analyze,
166
166
  participant_context: buildParticipantContext(context.personaId, state),
167
+ technical_context: (context.sources?.length ?? 0) > 0,
167
168
  });
168
169
 
169
170
  state.queue_enqueue({
@@ -176,7 +177,7 @@ export function queueTopicScan(context: ExtractionContext, state: StateManager,
176
177
  data: {
177
178
  ...options,
178
179
  personaId: chunk.personaId,
179
- personaDisplayName: chunk.personaDisplayName,
180
+ personaDisplayName: chunk.channelDisplayName,
180
181
  analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
181
182
  extraction_flag: context.extraction_flag,
182
183
  message_ids_to_mark: chunk.messages_analyze.map(m => m.id),
@@ -209,7 +210,7 @@ export function queuePersonScan(context: ExtractionContext, state: StateManager,
209
210
 
210
211
  for (const chunk of chunks) {
211
212
  const prompt = buildHumanPersonScanPrompt({
212
- persona_name: chunk.personaDisplayName,
213
+ persona_name: chunk.channelDisplayName,
213
214
  messages_context: chunk.messages_context,
214
215
  messages_analyze: chunk.messages_analyze,
215
216
  participant_context: buildParticipantContext(context.personaId, state),
@@ -226,7 +227,7 @@ export function queuePersonScan(context: ExtractionContext, state: StateManager,
226
227
  data: {
227
228
  ...options,
228
229
  personaId: chunk.personaId,
229
- personaDisplayName: chunk.personaDisplayName,
230
+ personaDisplayName: chunk.channelDisplayName,
230
231
  analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
231
232
  extraction_flag: context.extraction_flag,
232
233
  message_ids_to_mark: chunk.messages_analyze.map(m => m.id),
@@ -273,8 +274,9 @@ export function queueDirectTopicUpdate(
273
274
  existing_item: topic,
274
275
  messages_context: chunk.messages_context,
275
276
  messages_analyze: chunk.messages_analyze,
276
- persona_name: chunk.personaDisplayName,
277
+ persona_name: chunk.channelDisplayName,
277
278
  participant_context: buildParticipantContext(context.personaId, state),
279
+ technical_context: (context.sources?.length ?? 0) > 0,
278
280
  });
279
281
 
280
282
  state.queue_enqueue({
@@ -286,11 +288,12 @@ export function queueDirectTopicUpdate(
286
288
  next_step: LLMNextStep.HandleTopicUpdate,
287
289
  data: {
288
290
  personaId: context.personaId,
289
- personaDisplayName: context.personaDisplayName,
291
+ personaDisplayName: context.channelDisplayName,
290
292
  isNewItem: false,
291
293
  existingItemId: topic.id,
292
294
  analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
293
295
  extraction_model: extractionModel,
296
+ sources: context.sources,
294
297
  },
295
298
  });
296
299
  }
@@ -306,7 +309,7 @@ const EMBEDDING_MIN_SIMILARITY = 0.3;
306
309
  * Higher than EMBEDDING_MIN_SIMILARITY (0.3) because we need near-duplicates,
307
310
  * not just vague thematic overlap.
308
311
  */
309
- export const VALIDATE_MIN_SIMILARITY = 0.85;
312
+ export const VALIDATE_MIN_SIMILARITY = 0.92;
310
313
 
311
314
  /**
312
315
  * Queue a topic match request using embedding-based similarity (topics only).
@@ -423,8 +426,9 @@ export function queueTopicUpdate(
423
426
  new_topic_category: isNewItem ? context.candidateCategory : undefined,
424
427
  messages_context: chunk.messages_context,
425
428
  messages_analyze: chunk.messages_analyze,
426
- persona_name: chunk.personaDisplayName,
429
+ persona_name: chunk.channelDisplayName,
427
430
  participant_context: buildParticipantContext(primaryPersonaId, state),
431
+ technical_context: (context.sources?.length ?? 0) > 0,
428
432
  });
429
433
 
430
434
  state.queue_enqueue({
@@ -502,7 +506,7 @@ export function queueEventSummary(
502
506
 
503
507
  const context: ExtractionContext = {
504
508
  personaId,
505
- personaDisplayName: persona.display_name,
509
+ channelDisplayName: persona.display_name,
506
510
  messages_context,
507
511
  messages_analyze: windowMessages,
508
512
  extraction_flag: "e",
@@ -512,7 +516,7 @@ export function queueEventSummary(
512
516
 
513
517
  for (const chunk of chunks) {
514
518
  const prompt = buildEventScanPrompt({
515
- persona_name: chunk.personaDisplayName,
519
+ persona_name: chunk.channelDisplayName,
516
520
  messages_context: chunk.messages_context,
517
521
  messages_analyze: chunk.messages_analyze,
518
522
  participant_context: buildParticipantContext(personaId, state),
@@ -528,7 +532,7 @@ export function queueEventSummary(
528
532
  data: {
529
533
  ...options,
530
534
  personaId: chunk.personaId,
531
- personaDisplayName: chunk.personaDisplayName,
535
+ personaDisplayName: chunk.channelDisplayName,
532
536
  extraction_flag: "e",
533
537
  message_ids_to_mark: chunk.messages_analyze.map(m => m.id),
534
538
  },
@@ -595,7 +599,7 @@ export function queuePersonUpdate(
595
599
  new_person_relationship: isNewItem ? context.candidateRelationship : undefined,
596
600
  messages_context: chunk.messages_context,
597
601
  messages_analyze: chunk.messages_analyze,
598
- persona_name: chunk.personaDisplayName,
602
+ persona_name: chunk.channelDisplayName,
599
603
  participant_context: buildParticipantContext(primaryPersonaIdForUpdate, state),
600
604
  known_identifier_types: userIdentifierTypes,
601
605
  });
@@ -609,7 +613,7 @@ export function queuePersonUpdate(
609
613
  next_step: LLMNextStep.HandlePersonUpdate,
610
614
  data: {
611
615
  personaId: context.personaId,
612
- personaDisplayName: context.personaDisplayName,
616
+ personaDisplayName: context.channelDisplayName,
613
617
  roomId: context.roomId,
614
618
  isNewItem,
615
619
  existingItemId: existingItem?.id,
@@ -673,7 +677,7 @@ export function queueTargetedPersonUpdate(
673
677
  extraction_model?: string;
674
678
  } = {
675
679
  personaId: contextPersonaId,
676
- personaDisplayName: displayName,
680
+ channelDisplayName: displayName,
677
681
  messages_context: [],
678
682
  messages_analyze: allMessages,
679
683
  candidateName: existingItem.name,
@@ -728,7 +732,7 @@ export function queueTargetedTopicUpdate(
728
732
  const model = state.getHuman().settings?.default_model;
729
733
  const context: ExtractionContext = {
730
734
  personaId: contextPersonaId,
731
- personaDisplayName: displayName,
735
+ channelDisplayName: displayName,
732
736
  messages_context: [],
733
737
  messages_analyze: allMessages,
734
738
  roomId,
@@ -24,7 +24,6 @@ export {
24
24
  runHumanCeremony,
25
25
  } from "./ceremony.js";
26
26
  export { queueUserDedupRequest } from "./dedup-phase.js";
27
- export { queuePersonMigration } from "./person-migration.js";
28
27
  export {
29
28
  queuePersonaTopicRating,
30
29
  type PersonaTopicContext,
@@ -38,7 +38,7 @@ export function queuePersonaTopicRating(
38
38
  const { chunks } = chunkExtractionContext(
39
39
  {
40
40
  personaId: context.personaId,
41
- personaDisplayName: context.personaDisplayName,
41
+ channelDisplayName: context.personaDisplayName,
42
42
  messages_context: context.messages_context,
43
43
  messages_analyze: context.messages_analyze,
44
44
  },
@@ -78,7 +78,7 @@ function queueRoomTopicScan(
78
78
  ): void {
79
79
  const context: HumanExtractionContext = {
80
80
  personaId: roomId,
81
- personaDisplayName: roomDisplayName,
81
+ channelDisplayName: roomDisplayName,
82
82
  messages_context,
83
83
  messages_analyze,
84
84
  extraction_flag: "t",
@@ -122,7 +122,7 @@ function queueRoomPersonScan(
122
122
  ): void {
123
123
  const context: HumanExtractionContext = {
124
124
  personaId: roomId,
125
- personaDisplayName: roomDisplayName,
125
+ channelDisplayName: roomDisplayName,
126
126
  messages_context,
127
127
  messages_analyze,
128
128
  extraction_flag: "p",
@@ -180,7 +180,7 @@ function queueRoomEventScan(
180
180
  );
181
181
  const context: HumanExtractionContext = {
182
182
  personaId: roomId,
183
- personaDisplayName: roomDisplayName,
183
+ channelDisplayName: roomDisplayName,
184
184
  messages_context,
185
185
  messages_analyze: windowMessages,
186
186
  extraction_flag: "e",
@@ -348,7 +348,7 @@ export function queuePersonaCapture(state: StateManager, personaId: string): voi
348
348
  );
349
349
  const context: HumanExtractionContext = {
350
350
  personaId,
351
- personaDisplayName: persona.display_name,
351
+ channelDisplayName: persona.display_name,
352
352
  messages_context,
353
353
  messages_analyze: unextractedT,
354
354
  };
@@ -362,7 +362,7 @@ export function queuePersonaCapture(state: StateManager, personaId: string): voi
362
362
  );
363
363
  const context: HumanExtractionContext = {
364
364
  personaId,
365
- personaDisplayName: persona.display_name,
365
+ channelDisplayName: persona.display_name,
366
366
  messages_context,
367
367
  messages_analyze: unextractedP,
368
368
  };
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  RESERVED_PERSONA_NAMES,
3
3
  isReservedPersonaName,
4
+ isReservedPersonaId,
4
5
  type PersonaSummary,
5
6
  type PersonaEntity,
6
7
  type PersonaCreationInput,
@@ -110,6 +111,9 @@ export async function deletePersona(
110
111
  personaId: string,
111
112
  _deleteHumanData: boolean
112
113
  ): Promise<boolean> {
114
+ if (isReservedPersonaId(personaId)) {
115
+ throw new Error(`Cannot delete reserved persona "${personaId}". Use archive instead.`);
116
+ }
113
117
  const persona = sm.persona_getById(personaId);
114
118
  if (!persona) return false;
115
119
  sm.persona_delete(personaId);