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.
- package/README.md +22 -3
- package/package.json +8 -1
- package/src/README.md +10 -26
- package/src/core/context-utils.ts +2 -2
- package/src/core/handlers/document-segmentation.ts +113 -0
- package/src/core/handlers/heartbeat.ts +9 -1
- package/src/core/handlers/human-extraction.ts +4 -1
- package/src/core/handlers/human-matching.ts +5 -53
- package/src/core/handlers/index.ts +3 -51
- package/src/core/handlers/persona-generation.ts +1 -28
- package/src/core/handlers/rewrite.ts +13 -9
- package/src/core/handlers/utils.ts +2 -9
- package/src/core/heartbeat-manager.ts +5 -5
- package/src/core/llm-client.ts +11 -1
- package/src/core/message-manager.ts +26 -23
- package/src/core/orchestrators/ceremony.ts +87 -49
- package/src/core/orchestrators/extraction-chunker.ts +3 -3
- package/src/core/orchestrators/human-extraction.ts +22 -18
- package/src/core/orchestrators/index.ts +0 -1
- package/src/core/orchestrators/persona-topics.ts +1 -1
- package/src/core/orchestrators/room-extraction.ts +5 -5
- package/src/core/persona-manager.ts +4 -0
- package/src/core/processor.ts +98 -22
- package/src/core/prompt-context-builder.ts +7 -6
- package/src/core/queue-manager.ts +35 -0
- package/src/core/state/personas.ts +1 -17
- package/src/core/state/queue.ts +9 -1
- package/src/core/state-manager.ts +4 -66
- package/src/core/types/entities.ts +17 -3
- package/src/core/types/enums.ts +1 -2
- package/src/core/types/integrations.ts +2 -0
- package/src/core/types/llm.ts +9 -0
- package/src/core/types/rooms.ts +1 -1
- package/src/integrations/claude-code/importer.ts +1 -1
- package/src/integrations/cursor/importer.ts +1 -1
- package/src/integrations/document/chunker.ts +88 -0
- package/src/integrations/document/importer.ts +82 -0
- package/src/integrations/document/index.ts +2 -0
- package/src/integrations/document/invoice.ts +63 -0
- package/src/integrations/document/types.ts +16 -0
- package/src/integrations/document/unsource.ts +164 -0
- package/src/integrations/opencode/importer.ts +1 -1
- package/src/integrations/persona-history/importer.ts +197 -0
- package/src/integrations/persona-history/index.ts +3 -0
- package/src/integrations/persona-history/types.ts +7 -0
- package/src/prompts/ceremony/dedup.ts +7 -3
- package/src/prompts/ceremony/index.ts +2 -11
- package/src/prompts/ceremony/people-rewrite.ts +190 -0
- package/src/prompts/ceremony/{rewrite.ts → topic-rewrite.ts} +103 -78
- package/src/prompts/ceremony/types.ts +1 -42
- package/src/prompts/generation/index.ts +0 -3
- package/src/prompts/generation/types.ts +0 -15
- package/src/prompts/heartbeat/check.ts +18 -6
- package/src/prompts/heartbeat/types.ts +2 -1
- package/src/prompts/human/index.ts +0 -2
- package/src/prompts/human/person-scan.ts +13 -4
- package/src/prompts/human/topic-scan.ts +16 -2
- package/src/prompts/human/topic-update.ts +36 -4
- package/src/prompts/human/types.ts +1 -16
- package/src/prompts/index.ts +0 -19
- package/src/prompts/reflection/index.ts +35 -5
- package/src/prompts/reflection/types.ts +1 -1
- package/src/prompts/response/index.ts +5 -0
- package/src/prompts/response/sections.ts +26 -0
- package/src/prompts/response/types.ts +3 -0
- package/src/storage/indexed.ts +4 -0
- package/src/storage/interface.ts +1 -0
- package/src/storage/local.ts +4 -0
- package/src/templates/emmett.ts +49 -0
- package/tui/README.md +22 -0
- package/tui/src/app.tsx +9 -6
- package/tui/src/commands/delete.tsx +7 -1
- package/tui/src/commands/import.tsx +30 -0
- package/tui/src/commands/registry.test.ts +10 -5
- package/tui/src/commands/unsource.tsx +115 -0
- package/tui/src/components/PromptInput.tsx +4 -0
- package/tui/src/components/WelcomeOverlay.tsx +58 -32
- package/tui/src/context/ei.tsx +80 -60
- package/tui/src/globals.d.ts +57 -0
- package/tui/src/index.tsx +14 -0
- package/tui/src/storage/file.ts +11 -5
- package/tui/src/util/e2e-flags.ts +4 -3
- package/tui/src/util/help-content.ts +20 -0
- package/tui/src/util/provider-detection.ts +251 -0
- package/tui/src/util/yaml-human.ts +7 -1
- package/tui/src/util/yaml-persona.ts +8 -4
- package/tui/src/util/yaml-settings.ts +3 -3
- package/src/core/orchestrators/person-migration.ts +0 -55
- package/src/prompts/ceremony/description-check.ts +0 -54
- package/src/prompts/ceremony/expire.ts +0 -37
- package/src/prompts/ceremony/explore.ts +0 -77
- package/src/prompts/ceremony/person-migration.ts +0 -77
- package/src/prompts/generation/descriptions.ts +0 -91
- package/src/prompts/human/fact-scan.ts +0 -150
package/src/core/processor.ts
CHANGED
|
@@ -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.
|
|
881
|
-
human.settings.
|
|
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
|
|
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
|
|
1205
|
-
persona.
|
|
1206
|
-
?? this.stateManager.getHuman().settings?.
|
|
1207
|
-
??
|
|
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
|
-
|
|
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
|
|
322
|
-
?? human.settings?.
|
|
323
|
-
??
|
|
324
|
-
const windowCutoff = new Date(Date.now() -
|
|
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
|
|
338
|
-
const windowCutoff = new Date(Date.now() -
|
|
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
|
|
16
|
+
{ entity: data.entity, messages: data.messages },
|
|
33
17
|
])
|
|
34
18
|
);
|
|
35
19
|
}
|
package/src/core/state/queue.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/core/types/enums.ts
CHANGED
|
@@ -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 {
|
package/src/core/types/llm.ts
CHANGED
|
@@ -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 {
|
package/src/core/types/rooms.ts
CHANGED
|
@@ -35,7 +35,7 @@ export interface RoomEntity {
|
|
|
35
35
|
created_at: string;
|
|
36
36
|
last_updated: string;
|
|
37
37
|
capture_used?: boolean;
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|