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
@@ -39,7 +39,9 @@ import { ContextStatus as ContextStatusEnum, RoomMode } from "./types.js";
39
39
  import { registerReadMemoryExecutor, registerFileReadExecutor } from "./tools/index.js";
40
40
  import { createReadMemoryExecutor } from "./tools/builtin/read-memory.js";
41
41
  import { EI_WELCOME_MESSAGE, EI_PERSONA_DEFINITION } from "../templates/welcome.js";
42
+ import { EMMETT_PERSONA_DEFINITION } from "../templates/emmett.js";
42
43
  import { shouldStartCeremony, startCeremony, handleCeremonyProgress, queueReflectionDrain, queueUserDedupRequest, queueRoomCapture, queuePersonaCapture, checkAndQueueRoomExtraction, queueTargetedPersonUpdate, queueTargetedTopicUpdate } from "./orchestrators/index.js";
44
+ import { finishDocumentBatch } from "./handlers/document-segmentation.js";
43
45
  import { BUILT_IN_FACTS } from "./constants/built-in-facts.js";
44
46
  import { DEFAULT_SEED_TRAITS } from "./constants/seed-traits.js";
45
47
 
@@ -132,6 +134,8 @@ import {
132
134
  markAllRoomMessagesRead,
133
135
  } from "./room-manager.js";
134
136
  import type { RoomCreationInput, RoomEntity, RoomMessage, RoomSummary } from "./types.js";
137
+ import { previewUnsource as _previewUnsource } from "../integrations/document/unsource.js";
138
+ import type { UnsourcePreview, UnsourceResult } from "../integrations/document/unsource.js";
135
139
 
136
140
  const DEFAULT_LOOP_INTERVAL_MS = 100;
137
141
  const DEFAULT_OPENCODE_POLLING_MS = 60000;
@@ -282,6 +286,46 @@ export class Processor {
282
286
  this.interface.onMessageAdded?.(eiEntity.id);
283
287
  }
284
288
 
289
+ private bootstrapEmmett(): void {
290
+ const existing = this.stateManager.persona_getById("emmet");
291
+ if (existing) {
292
+ if (existing.is_archived) {
293
+ this.stateManager.persona_unarchive("emmet");
294
+ }
295
+ return;
296
+ }
297
+ const readMemoryTool = this.stateManager.tools_getByName("read_memory");
298
+ const emmettEntity: PersonaEntity = {
299
+ ...EMMETT_PERSONA_DEFINITION,
300
+ id: "emmet",
301
+ display_name: "Emmett",
302
+ last_updated: new Date().toISOString(),
303
+ tools: readMemoryTool ? [readMemoryTool.id] : [],
304
+ };
305
+ this.stateManager.persona_add(emmettEntity);
306
+ this.interface.onPersonaAdded?.();
307
+ }
308
+
309
+ async importDocument(content: string, filename: string): Promise<import("../integrations/document/types.js").DocumentImportResult> {
310
+ this.bootstrapEmmett();
311
+ const { importDocument } = await import("../integrations/document/importer.js");
312
+ return importDocument({
313
+ stateManager: this.stateManager,
314
+ interface: this.interface,
315
+ content,
316
+ filename,
317
+ });
318
+ }
319
+
320
+ getUnsourcePreview(sourceTag: string): UnsourcePreview {
321
+ return _previewUnsource(sourceTag, this.stateManager);
322
+ }
323
+
324
+ async executeUnsource(preview: UnsourcePreview): Promise<UnsourceResult> {
325
+ const { executeUnsource } = await import("../integrations/document/unsource.js");
326
+ return executeUnsource(preview, this.stateManager);
327
+ }
328
+
285
329
  /**
286
330
  * Seed built-in tool providers and tools if they don't exist yet.
287
331
  * Called on every startup (after state load/restore) — safe to call repeatedly.
@@ -877,8 +921,8 @@ export class Processor {
877
921
  modified = true;
878
922
  }
879
923
 
880
- if (human.settings.default_context_window_hours == null) {
881
- human.settings.default_context_window_hours = 8;
924
+ if (human.settings.default_context_window_ms == null) {
925
+ human.settings.default_context_window_ms = 28800000;
882
926
  modified = true;
883
927
  }
884
928
 
@@ -1044,7 +1088,6 @@ const toolNextSteps = new Set([
1044
1088
  LLMNextStep.HandleEiHeartbeat,
1045
1089
  LLMNextStep.HandleToolContinuation,
1046
1090
  LLMNextStep.HandleDedupCurate,
1047
- LLMNextStep.HandlePersonIdentifierMigration,
1048
1091
  ]);
1049
1092
  const toolPersonaId =
1050
1093
  personaId ??
@@ -1059,13 +1102,8 @@ const toolNextSteps = new Set([
1059
1102
  (request.next_step === LLMNextStep.HandleToolContinuation &&
1060
1103
  request.data.originalNextStep === LLMNextStep.HandleDedupCurate);
1061
1104
 
1062
- const isPersonMigrationRequest =
1063
- request.next_step === LLMNextStep.HandlePersonIdentifierMigration ||
1064
- (request.next_step === LLMNextStep.HandleToolContinuation &&
1065
- request.data.originalNextStep === LLMNextStep.HandlePersonIdentifierMigration);
1066
-
1067
1105
  let tools: ToolDefinition[] = [];
1068
- if (isDedupRequest || isPersonMigrationRequest) {
1106
+ if (isDedupRequest) {
1069
1107
  const readMemory = this.stateManager.tools_getByName("read_memory");
1070
1108
  if (readMemory?.enabled) {
1071
1109
  tools = [readMemory];
@@ -1174,6 +1212,15 @@ const toolNextSteps = new Set([
1174
1212
  await this.checkAndSyncCursor(human, now);
1175
1213
  }
1176
1214
 
1215
+ if (
1216
+ this.isTUI &&
1217
+ human.settings?.personaHistory?.integration &&
1218
+ !human.settings.personaHistory.complete &&
1219
+ this.stateManager.queue_length() === 0
1220
+ ) {
1221
+ await this.checkAndSyncPersonaHistory(human);
1222
+ }
1223
+
1177
1224
  if (human.settings?.ceremony && shouldStartCeremony(human.settings.ceremony, this.stateManager)) {
1178
1225
  if (human.settings?.sync && remoteSync.isConfigured()) {
1179
1226
  const state = this.stateManager.getStorageState();
@@ -1186,7 +1233,7 @@ const toolNextSteps = new Set([
1186
1233
  }
1187
1234
 
1188
1235
  for (const persona of this.stateManager.persona_getAll()) {
1189
- if (persona.is_paused || persona.is_archived) continue;
1236
+ if (persona.is_paused || persona.is_archived || persona.is_static) continue;
1190
1237
 
1191
1238
  const defaultHeartbeatMs = this.stateManager.getHuman().settings?.default_heartbeat_ms ?? 1800000;
1192
1239
  const heartbeatDelay = persona.heartbeat_delay_ms ?? defaultHeartbeatMs;
@@ -1201,14 +1248,14 @@ const toolNextSteps = new Set([
1201
1248
 
1202
1249
  if (timeSinceHeartbeat >= heartbeatDelay) {
1203
1250
  const history = this.stateManager.messages_get(persona.id);
1204
- const contextWindowHours =
1205
- persona.context_window_hours
1206
- ?? this.stateManager.getHuman().settings?.default_context_window_hours
1207
- ?? 8;
1251
+ const contextWindowMs =
1252
+ persona.context_window_ms
1253
+ ?? this.stateManager.getHuman().settings?.default_context_window_ms
1254
+ ?? 28800000;
1208
1255
  const contextHistory = filterMessagesForContext(
1209
1256
  history,
1210
1257
  persona.context_boundary,
1211
- contextWindowHours
1258
+ contextWindowMs
1212
1259
  );
1213
1260
  const trailing = countTrailingPersonaMessages(contextHistory);
1214
1261
  if (trailing < 3) {
@@ -1414,6 +1461,32 @@ const toolNextSteps = new Set([
1414
1461
  });
1415
1462
  }
1416
1463
 
1464
+ private personaHistoryImportInProgress = false;
1465
+
1466
+ private async checkAndSyncPersonaHistory(_human: HumanEntity): Promise<void> {
1467
+ if (this.personaHistoryImportInProgress) return;
1468
+
1469
+ this.personaHistoryImportInProgress = true;
1470
+ import("../integrations/persona-history/importer.js")
1471
+ .then(({ importPersonaHistory }) =>
1472
+ importPersonaHistory({ stateManager: this.stateManager })
1473
+ )
1474
+ .then((result) => {
1475
+ if (result.scansQueued > 0) {
1476
+ console.log(
1477
+ `[Processor] PersonaHistory: ${result.scansQueued} scans queued` +
1478
+ (result.complete ? " — import complete" : "")
1479
+ );
1480
+ }
1481
+ })
1482
+ .catch((err) => {
1483
+ console.warn(`[Processor] PersonaHistory sync failed:`, err);
1484
+ })
1485
+ .finally(() => {
1486
+ this.personaHistoryImportInProgress = false;
1487
+ });
1488
+ }
1489
+
1417
1490
  private augmentRoomRequest(request: LLMRequest): LLMRequest {
1418
1491
  if (request.next_step !== LLMNextStep.HandleRoomResponse) return request;
1419
1492
 
@@ -1621,13 +1694,6 @@ const toolNextSteps = new Set([
1621
1694
  }
1622
1695
  }
1623
1696
 
1624
- if (response.request.next_step === LLMNextStep.HandlePersonaDescriptions) {
1625
- const personaId = response.request.data.personaId as string;
1626
- if (personaId) {
1627
- this.interface.onPersonaUpdated?.(personaId);
1628
- }
1629
- }
1630
-
1631
1697
  if (
1632
1698
  response.request.next_step === LLMNextStep.HandlePersonaTraitExtraction ||
1633
1699
  response.request.next_step === LLMNextStep.HandlePersonaTopicRating
@@ -1688,6 +1754,16 @@ const toolNextSteps = new Set([
1688
1754
  if (typeof response.request.data.ceremony_progress === "number") {
1689
1755
  handleCeremonyProgress(this.stateManager, response.request.data.ceremony_progress);
1690
1756
  }
1757
+
1758
+ if (response.request.next_step === LLMNextStep.HandleDocumentSegmentation) {
1759
+ const batchId = response.request.data.batchId as string;
1760
+ const filename = response.request.data.filename as string;
1761
+ if (batchId && !this.stateManager.queue_hasPendingDocumentSegments(batchId)) {
1762
+ finishDocumentBatch(batchId, filename, this.stateManager);
1763
+ this.interface.onMessageAdded?.("emmet");
1764
+ this.interface.onHumanUpdated?.();
1765
+ }
1766
+ }
1691
1767
  } catch (err) {
1692
1768
  const errorMsg = err instanceof Error ? err.message : String(err);
1693
1769
  const result = this.stateManager.queue_fail(response.request.id, errorMsg);
@@ -291,6 +291,7 @@ export async function buildResponsePromptData(
291
291
  topics: persona.topics,
292
292
  interested_topics: persona.topics.filter(t => t.exposure_desired - t.exposure_current > 0.2),
293
293
  include_message_timestamps: persona.include_message_timestamps,
294
+ pending_update: persona.pending_update,
294
295
  },
295
296
  human: filteredHuman,
296
297
  visible_personas: visiblePersonas,
@@ -318,10 +319,10 @@ export async function buildRoomResponsePromptData(
318
319
 
319
320
  let sourceMessages: RoomMessage[];
320
321
  if (room.mode === RoomMode.FreeForAll) {
321
- const contextWindowHours = room.context_window_hours
322
- ?? human.settings?.default_context_window_hours
323
- ?? 8;
324
- const windowCutoff = new Date(Date.now() - contextWindowHours * 60 * 60 * 1000).toISOString();
322
+ const contextWindowMs = room.context_window_ms
323
+ ?? human.settings?.default_context_window_ms
324
+ ?? 28800000;
325
+ const windowCutoff = new Date(Date.now() - contextWindowMs).toISOString();
325
326
  const boundaryMs = room.context_boundary ? new Date(room.context_boundary).getTime() : 0;
326
327
  sourceMessages = allSourceMessages.filter(m => {
327
328
  const msgMs = new Date(m.timestamp).getTime();
@@ -334,8 +335,8 @@ export async function buildRoomResponsePromptData(
334
335
  const byCount = allSourceMessages.slice(-MIN_ROOM_MESSAGES);
335
336
  if (byCount.length > sourceMessages.length) sourceMessages = byCount;
336
337
  } else {
337
- const contextWindowHours = human.settings?.default_context_window_hours ?? 8;
338
- const windowCutoff = new Date(Date.now() - contextWindowHours * 60 * 60 * 1000).toISOString();
338
+ const contextWindowMs = human.settings?.default_context_window_ms ?? 28800000;
339
+ const windowCutoff = new Date(Date.now() - contextWindowMs).toISOString();
339
340
  const byTime = allSourceMessages.filter(m => m.timestamp >= windowCutoff);
340
341
  const byCount = allSourceMessages.slice(-MIN_ROOM_MESSAGES);
341
342
  sourceMessages = byTime.length >= byCount.length ? byTime : byCount;
@@ -18,6 +18,39 @@ export async function resumeQueue(sm: StateManager): Promise<void> {
18
18
  }
19
19
 
20
20
  export async function getQueueStatus(sm: StateManager): Promise<QueueStatus> {
21
+ const activeItems = sm.queue_getAllActiveItems();
22
+ const segmentationItems = activeItems.filter(
23
+ r => r.next_step === LLMNextStep.HandleDocumentSegmentation
24
+ );
25
+
26
+ const batchMap = new Map<string, { filename: string; count: number }>();
27
+ for (const item of segmentationItems) {
28
+ const { batchId, filename } = item.data as { batchId: string; filename: string };
29
+ if (!batchId || !filename) continue;
30
+ const existing = batchMap.get(batchId);
31
+ if (existing) {
32
+ existing.count++;
33
+ } else {
34
+ batchMap.set(batchId, { filename, count: 1 });
35
+ }
36
+ }
37
+
38
+ const pending_documents = batchMap.size > 0
39
+ ? Array.from(batchMap.entries()).map(([batchId, { filename, count }]) => ({ batchId, filename, count }))
40
+ : undefined;
41
+
42
+ const extractingSet = new Set<string>();
43
+ for (const item of activeItems) {
44
+ const sources = item.data.sources as string[] | undefined;
45
+ if (!Array.isArray(sources)) continue;
46
+ for (const s of sources) {
47
+ if (typeof s === "string" && s.startsWith("import:document:")) {
48
+ extractingSet.add(s.slice("import:document:".length));
49
+ }
50
+ }
51
+ }
52
+ const extracting_documents = extractingSet.size > 0 ? Array.from(extractingSet) : undefined;
53
+
21
54
  return {
22
55
  state: sm.queue_isPaused()
23
56
  ? "paused"
@@ -27,6 +60,8 @@ export async function getQueueStatus(sm: StateManager): Promise<QueueStatus> {
27
60
  pending_count: sm.queue_length(),
28
61
  dlq_count: sm.queue_dlqLength(),
29
62
  embedding_warning: sm.embedding_getWarning() || undefined,
63
+ pending_documents,
64
+ extracting_documents,
30
65
  };
31
66
  }
32
67
 
@@ -1,21 +1,5 @@
1
1
  import type { PersonaEntity, Message, ContextStatus } from "../types.js";
2
2
 
3
- // TODO(v1.0.0): Remove LegacyMessage migration — verbal_response/action_response no longer written
4
- type LegacyMessage = Message & { verbal_response?: string; action_response?: string };
5
-
6
- function migrateMessage(msg: Message): Message {
7
- if (msg.content) return msg;
8
- if (msg.silence_reason) return msg;
9
- const legacy = msg as LegacyMessage;
10
- const hasLegacy = 'verbal_response' in legacy || 'action_response' in legacy;
11
- if (!hasLegacy) return msg;
12
- const parts: string[] = [];
13
- if (legacy.action_response) parts.push(`_${legacy.action_response}_`);
14
- if (legacy.verbal_response) parts.push(legacy.verbal_response);
15
- const { verbal_response: _vr, action_response: _ar, ...rest } = legacy;
16
- return parts.length > 0 ? { ...rest, content: parts.join('\n\n') } : rest as Message;
17
- }
18
-
19
3
  export interface PersonaData {
20
4
  entity: PersonaEntity;
21
5
  messages: Message[];
@@ -29,7 +13,7 @@ export class PersonaState {
29
13
  this.personas = new Map(
30
14
  Object.entries(personas).map(([id, data]) => [
31
15
  id,
32
- { entity: data.entity, messages: data.messages.map(migrateMessage) },
16
+ { entity: data.entity, messages: data.messages },
33
17
  ])
34
18
  );
35
19
  }
@@ -1,5 +1,5 @@
1
1
  import type { LLMRequest, QueueFailResult } from "../types.js";
2
- import { DLQ_MAX_COUNT, DLQ_MAX_AGE_DAYS } from "../types.js";
2
+ import { DLQ_MAX_COUNT, DLQ_MAX_AGE_DAYS, LLMNextStep } from "../types.js";
3
3
 
4
4
  const BASE_BACKOFF_MS = 2_000;
5
5
  const MAX_BACKOFF_MS = 30_000;
@@ -200,6 +200,14 @@ export class QueueState {
200
200
  return this.queue.some(r => r.state !== "dlq" && typeof r.data.ceremony_progress === "number" && r.data.ceremony_progress > 0);
201
201
  }
202
202
 
203
+ hasPendingDocumentSegments(batchId: string): boolean {
204
+ return this.queue.some(r =>
205
+ r.state !== "dlq" &&
206
+ r.next_step === LLMNextStep.HandleDocumentSegmentation &&
207
+ r.data.batchId === batchId
208
+ );
209
+ }
210
+
203
211
  clear(): number {
204
212
  const count = this.queue.filter(r => r.state !== "dlq").length;
205
213
  this.queue = this.queue.filter(r => r.state === "dlq");
@@ -69,76 +69,10 @@ export class StateManager {
69
69
  this.migrateMessageFlags();
70
70
  this.migrateInterestedPersonas();
71
71
  this.migrateProviderModel();
72
- this.migratePersonaMessageContent();
73
- this.migrateRoomMessageContent();
74
72
  this.migrateThemes();
75
73
  this.migrateFfaParentIds();
76
74
  }
77
75
 
78
- private migratePersonaMessageContent(): void {
79
- // TODO(v1.0.0): Remove legacy persona message migration — verbal_response/action_response no longer written
80
- const rawPersonas = (this.personaState as unknown as { personas: Map<string, { messages: Message[] }> }).personas;
81
- let migratedCount = 0;
82
- for (const [, data] of rawPersonas) {
83
- const messages = data.messages;
84
- for (const msg of messages) {
85
- const legacy = msg as Message & { verbal_response?: string; action_response?: string };
86
- if (!('verbal_response' in legacy || 'action_response' in legacy)) continue;
87
- if (msg.content) {
88
- delete (legacy as any).verbal_response;
89
- delete (legacy as any).action_response;
90
- migratedCount++;
91
- continue;
92
- }
93
- if (msg.silence_reason) {
94
- delete (legacy as any).verbal_response;
95
- delete (legacy as any).action_response;
96
- migratedCount++;
97
- continue;
98
- }
99
- const parts: string[] = [];
100
- if (legacy.action_response) parts.push(`_${legacy.action_response}_`);
101
- if (legacy.verbal_response) parts.push(legacy.verbal_response);
102
- if (parts.length > 0) (msg as any).content = parts.join('\n\n');
103
- delete (legacy as any).verbal_response;
104
- delete (legacy as any).action_response;
105
- migratedCount++;
106
- }
107
- }
108
- if (migratedCount > 0) {
109
- this.scheduleSave();
110
- console.log(`[StateManager] Migrated ${migratedCount} persona messages to unified content field`);
111
- }
112
- }
113
-
114
- private migrateRoomMessageContent(): void {
115
- const rooms = this.roomState.getAll(true);
116
- let migratedCount = 0;
117
-
118
- for (const room of rooms) {
119
- for (const msg of room.messages) {
120
- if (msg.content) continue;
121
- if (msg.silence_reason) continue;
122
- // TODO(v1.0.0): Remove legacy room message migration — verbal_response/action_response no longer written
123
- const legacy = msg as RoomMessage & { verbal_response?: string; action_response?: string };
124
- const hasLegacy = 'verbal_response' in legacy || 'action_response' in legacy;
125
- if (!hasLegacy) continue;
126
- const parts: string[] = [];
127
- if (legacy.action_response) parts.push(`_${legacy.action_response}_`);
128
- if (legacy.verbal_response) parts.push(legacy.verbal_response);
129
- if (parts.length > 0) msg.content = parts.join('\n\n');
130
- delete (msg as any).verbal_response;
131
- delete (msg as any).action_response;
132
- migratedCount++;
133
- }
134
- }
135
-
136
- if (migratedCount > 0) {
137
- this.scheduleSave();
138
- console.log(`[StateManager] Migrated ${migratedCount} room messages to unified content field`);
139
- }
140
- }
141
-
142
76
  /**
143
77
  * Migration: learned_by used to store display names; now stores persona IDs.
144
78
  * On load, attempt to resolve display names -> IDs using current persona map.
@@ -983,6 +917,10 @@ export class StateManager {
983
917
  return this.queueState.hasPendingCeremonies();
984
918
  }
985
919
 
920
+ queue_hasPendingDocumentSegments(batchId: string): boolean {
921
+ return this.queueState.hasPendingDocumentSegments(batchId);
922
+ }
923
+
986
924
  queue_clear(): number {
987
925
  const result = this.queueState.clear();
988
926
  this.scheduleSave();
@@ -20,6 +20,11 @@ export interface OpenCodeSettings {
20
20
  processed_sessions?: Record<string, string>; // sessionId → ISO timestamp of last import
21
21
  }
22
22
 
23
+ export interface DocumentSettings {
24
+ extraction_model?: string;
25
+ processed_documents?: Record<string, string>;
26
+ }
27
+
23
28
  export interface CeremonyConfig {
24
29
  time: string; // "HH:MM" format (e.g., "09:00")
25
30
  last_ceremony?: string; // ISO timestamp
@@ -103,12 +108,11 @@ export interface HumanSettings {
103
108
  default_model?: string; // Will store ModelConfig.id GUID post-migration
104
109
  oneshot_model?: string; // Model for AI-assist (wand) requests; falls back to default_model. Will store ModelConfig.id GUID post-migration.
105
110
  rewrite_model?: string; // Model for rewrite ceremony step; must be capable (Sonnet/Opus class). Unset = rewrite disabled. Will store ModelConfig.id GUID post-migration.
106
- people_migration_complete?: boolean; // Set to true when all Person records have identifiers. Ceremony migration step short-circuits when true.
107
111
  queue_paused?: boolean;
108
112
  skip_quote_delete_confirm?: boolean;
109
113
  name_display?: string;
110
114
  default_heartbeat_ms?: number;
111
- default_context_window_hours?: number;
115
+ default_context_window_ms?: number;
112
116
  message_min_count?: number;
113
117
  message_max_age_days?: number;
114
118
  accounts?: ProviderAccount[];
@@ -118,8 +122,10 @@ export interface HumanSettings {
118
122
  backup?: BackupConfig;
119
123
  claudeCode?: import("../../integrations/claude-code/types.js").ClaudeCodeSettings;
120
124
  cursor?: import("../../integrations/cursor/types.js").CursorSettings;
125
+ document?: DocumentSettings;
121
126
  active_theme?: string;
122
127
  custom_themes?: ThemeDefinition[];
128
+ personaHistory?: import("../../integrations/persona-history/types.js").PersonaHistorySettings;
123
129
  }
124
130
 
125
131
  export interface HumanEntity {
@@ -150,7 +156,7 @@ export interface PersonaEntity {
150
156
  archived_at?: string;
151
157
  is_static: boolean;
152
158
  heartbeat_delay_ms?: number;
153
- context_window_hours?: number;
159
+ context_window_ms?: number;
154
160
  include_message_timestamps?: boolean; // Prepend ISO timestamp to each message sent to the LLM
155
161
  context_boundary?: string; // ISO timestamp - messages before this excluded from LLM context
156
162
  last_updated: string;
@@ -203,3 +209,11 @@ export type ReservedPersonaName = typeof RESERVED_PERSONA_NAMES[number];
203
209
  export function isReservedPersonaName(name: string): boolean {
204
210
  return RESERVED_PERSONA_NAMES.includes(name.toLowerCase() as ReservedPersonaName);
205
211
  }
212
+
213
+ // Reserved persona IDs (built-in system personas that cannot be deleted)
214
+ export const RESERVED_PERSONA_IDS = ["ei", "emmet"] as const;
215
+ export type ReservedPersonaId = typeof RESERVED_PERSONA_IDS[number];
216
+
217
+ export function isReservedPersonaId(id: string): boolean {
218
+ return (RESERVED_PERSONA_IDS as readonly string[]).includes(id);
219
+ }
@@ -26,7 +26,6 @@ export enum LLMPriority {
26
26
  export enum LLMNextStep {
27
27
  HandlePersonaResponse = "handlePersonaResponse",
28
28
  HandlePersonaGeneration = "handlePersonaGeneration",
29
- HandlePersonaDescriptions = "handlePersonaDescriptions",
30
29
  HandleFactFind = "handleFactFind",
31
30
  HandleHumanTopicScan = "handleHumanTopicScan",
32
31
  HandleHumanPersonScan = "handleHumanPersonScan",
@@ -51,9 +50,9 @@ export enum LLMNextStep {
51
50
  HandleRoomResponse = "handleRoomResponse",
52
51
  HandleRoomJudge = "handleRoomJudge",
53
52
  HandlePersonaPreview = "handlePersonaPreview",
54
- HandlePersonIdentifierMigration = "handlePersonIdentifierMigration",
55
53
  HandleTopicValidate = "handleTopicValidate",
56
54
  HandleReflectionCritic = "handleReflectionCritic",
55
+ HandleDocumentSegmentation = "handleDocumentSegmentation",
57
56
  }
58
57
 
59
58
  export enum ProviderType {
@@ -75,6 +75,8 @@ export interface QueueStatus {
75
75
  current_operation?: string;
76
76
  /** True when the embedding service failed and topic/person matching fell back to recent items. */
77
77
  embedding_warning?: boolean;
78
+ pending_documents?: Array<{ batchId: string; filename: string; count: number }>;
79
+ extracting_documents?: string[];
78
80
  }
79
81
 
80
82
  export interface EiError {
@@ -27,6 +27,15 @@ export interface Message {
27
27
 
28
28
  external?: boolean; // Set by integration importers (OpenCode, Cursor, Claude Code); invisible to LLM context
29
29
 
30
+ /**
31
+ * Integration source tag. Set ONLY on external: true messages by importers (document, Slack, etc.)
32
+ * to identify which external source this synthetic message came from.
33
+ * Format: "import:document:filename" | "slack:channelId" | etc.
34
+ * Enables quote provenance tracing: quote.message_id → message.source_tag → original source.
35
+ * Never set on conversational messages.
36
+ */
37
+ source_tag?: string;
38
+
30
39
  }
31
40
 
32
41
  export interface ChatMessage {
@@ -35,7 +35,7 @@ export interface RoomEntity {
35
35
  created_at: string;
36
36
  last_updated: string;
37
37
  capture_used?: boolean;
38
- context_window_hours?: number; // FFA only; falls back to human.settings.default_context_window_hours
38
+ context_window_ms?: number; // FFA only; falls back to human.settings.default_context_window_ms
39
39
  context_boundary?: string; // FFA only; ISO timestamp; same semantics as persona context_boundary
40
40
  messages: RoomMessage[];
41
41
  }
@@ -262,7 +262,7 @@ export async function importClaudeCodeSessions(
262
262
 
263
263
  const context: ExtractionContext = {
264
264
  personaId: persona.id,
265
- personaDisplayName: persona.display_name,
265
+ channelDisplayName: persona.display_name,
266
266
  messages_context: contextMsgs,
267
267
  messages_analyze: toAnalyze,
268
268
  sources: [`claudecode:${getMachineId()}:${targetSession.id}`],
@@ -221,7 +221,7 @@ export async function importCursorSessions(
221
221
 
222
222
  const context: ExtractionContext = {
223
223
  personaId: persona.id,
224
- personaDisplayName: persona.display_name,
224
+ channelDisplayName: persona.display_name,
225
225
  messages_context: contextMsgs,
226
226
  messages_analyze: toAnalyze,
227
227
  sources: [`cursor:${getMachineId()}:${targetSession.id}`],
@@ -0,0 +1,88 @@
1
+ import { expandToWordBoundaries } from "../../core/handlers/human-matching.js";
2
+
3
+ const DEFAULT_CHUNK_CHARS = 6000;
4
+ const DEFAULT_OVERLAP_CHARS = 300;
5
+
6
+ const MARKDOWN_SEPARATORS = ["\n## ", "\n### ", "\n#### ", "\n\n", "\n", ". ", " ", ""];
7
+ const DEFAULT_SEPARATORS = ["\n\n", "\n", ". ", " ", ""];
8
+
9
+ function splitOnSeparator(text: string, separator: string): string[] {
10
+ if (separator === "") {
11
+ return text.split("");
12
+ }
13
+ return text.split(separator);
14
+ }
15
+
16
+ function mergeChunks(pieces: string[], separator: string, chunkSize: number): string[] {
17
+ const merged: string[] = [];
18
+ let current = "";
19
+
20
+ for (const piece of pieces) {
21
+ const candidate = current ? current + separator + piece : piece;
22
+ if (candidate.length <= chunkSize) {
23
+ current = candidate;
24
+ } else {
25
+ if (current) merged.push(current);
26
+ current = piece.length <= chunkSize ? piece : piece;
27
+ }
28
+ }
29
+ if (current) merged.push(current);
30
+ return merged;
31
+ }
32
+
33
+ function recursiveSplit(
34
+ text: string,
35
+ separators: string[],
36
+ chunkSize: number
37
+ ): string[] {
38
+ if (text.length <= chunkSize) {
39
+ return [text];
40
+ }
41
+
42
+ const [separator, ...remainingSeparators] = separators;
43
+
44
+ if (separator === undefined) {
45
+ return [text];
46
+ }
47
+
48
+ const pieces = splitOnSeparator(text, separator);
49
+ const result: string[] = [];
50
+
51
+ for (const piece of pieces) {
52
+ if (piece.length <= chunkSize) {
53
+ result.push(piece);
54
+ } else if (remainingSeparators.length > 0) {
55
+ result.push(...recursiveSplit(piece, remainingSeparators, chunkSize));
56
+ } else {
57
+ result.push(piece);
58
+ }
59
+ }
60
+
61
+ return mergeChunks(result, separator, chunkSize);
62
+ }
63
+
64
+ function applyOverlap(chunks: string[], overlapChars: number): string[] {
65
+ if (overlapChars <= 0 || chunks.length <= 1) return chunks;
66
+
67
+ return chunks.map((chunk, i) => {
68
+ if (i === 0) return chunk;
69
+ const prev = chunks[i - 1];
70
+ const rawStart = Math.max(0, prev.length - overlapChars);
71
+ const { start } = expandToWordBoundaries(prev, rawStart, rawStart);
72
+ const prefix = prev.slice(start);
73
+ return prefix + chunk;
74
+ });
75
+ }
76
+
77
+ export function recursiveCharacterSplit(
78
+ text: string,
79
+ options?: { chunkSize?: number; overlap?: number; isMarkdown?: boolean }
80
+ ): string[] {
81
+ const chunkSize = options?.chunkSize ?? DEFAULT_CHUNK_CHARS;
82
+ const overlap = options?.overlap ?? DEFAULT_OVERLAP_CHARS;
83
+ const separators = options?.isMarkdown ? MARKDOWN_SEPARATORS : DEFAULT_SEPARATORS;
84
+
85
+ const rawChunks = recursiveSplit(text, separators, chunkSize);
86
+ const nonEmpty = rawChunks.filter(c => c.trim().length > 0);
87
+ return applyOverlap(nonEmpty, overlap);
88
+ }