ei-tui 0.1.3 → 0.1.5
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 +36 -35
- package/package.json +6 -2
- package/src/README.md +85 -1
- package/src/cli/README.md +30 -20
- package/src/cli/retrieval.ts +5 -17
- package/src/cli.ts +69 -0
- package/src/core/handlers/index.ts +195 -172
- package/src/core/orchestrators/ceremony.ts +4 -4
- package/src/core/orchestrators/extraction-chunker.ts +3 -3
- package/src/core/processor.ts +245 -77
- package/src/core/queue-processor.ts +3 -26
- package/src/core/state/checkpoints.ts +4 -0
- package/src/core/state/personas.ts +13 -1
- package/src/core/state/queue.ts +80 -23
- package/src/core/state-manager.ts +36 -10
- package/src/core/types.ts +23 -11
- package/src/core/utils/crossFind.ts +44 -0
- package/src/core/utils/index.ts +4 -0
- package/src/integrations/opencode/importer.ts +118 -691
- package/src/prompts/heartbeat/check.ts +27 -13
- package/src/prompts/heartbeat/ei.ts +65 -136
- package/src/prompts/heartbeat/types.ts +47 -17
- package/src/prompts/human/item-update.ts +20 -8
- package/src/prompts/index.ts +2 -5
- package/src/prompts/message-utils.ts +42 -3
- package/src/prompts/response/index.ts +13 -6
- package/src/prompts/response/sections.ts +65 -12
- package/src/prompts/response/types.ts +10 -0
- package/tui/README.md +89 -4
- package/tui/src/commands/dlq.ts +75 -0
- package/tui/src/commands/editor.tsx +1 -1
- package/tui/src/commands/queue.ts +77 -0
- package/tui/src/components/CommandSuggest.tsx +50 -0
- package/tui/src/components/MessageList.tsx +12 -2
- package/tui/src/components/PromptInput.tsx +118 -30
- package/tui/src/components/Sidebar.tsx +6 -2
- package/tui/src/components/StatusBar.tsx +12 -5
- package/tui/src/context/ei.tsx +43 -3
- package/tui/src/context/keyboard.tsx +90 -2
- package/tui/src/util/clipboard.ts +73 -0
- package/tui/src/util/yaml-serializers.ts +81 -11
- package/src/prompts/validation/ei.ts +0 -93
- package/src/prompts/validation/index.ts +0 -6
- package/src/prompts/validation/types.ts +0 -22
package/src/core/processor.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
LLMRequestType,
|
|
3
3
|
LLMPriority,
|
|
4
4
|
LLMNextStep,
|
|
5
|
+
ValidationLevel,
|
|
5
6
|
RESERVED_PERSONA_NAMES,
|
|
6
7
|
isReservedPersonaName,
|
|
7
8
|
type LLMRequest,
|
|
@@ -35,9 +36,12 @@ import {
|
|
|
35
36
|
buildResponsePrompt,
|
|
36
37
|
buildPersonaTraitExtractionPrompt,
|
|
37
38
|
buildHeartbeatCheckPrompt,
|
|
39
|
+
buildEiHeartbeatPrompt,
|
|
38
40
|
type ResponsePromptData,
|
|
39
41
|
type PersonaTraitExtractionPromptData,
|
|
40
42
|
type HeartbeatCheckPromptData,
|
|
43
|
+
type EiHeartbeatPromptData,
|
|
44
|
+
type EiHeartbeatItem,
|
|
41
45
|
} from "../prompts/index.js";
|
|
42
46
|
import {
|
|
43
47
|
orchestratePersonaGeneration,
|
|
@@ -52,6 +56,7 @@ import {
|
|
|
52
56
|
import { EI_WELCOME_MESSAGE, EI_PERSONA_DEFINITION } from "../templates/welcome.js";
|
|
53
57
|
import { getEmbeddingService, findTopK, needsEmbeddingUpdate, needsQuoteEmbeddingUpdate, computeDataItemEmbedding, computeQuoteEmbedding } from "./embedding-service.js";
|
|
54
58
|
import { ContextStatus as ContextStatusEnum } from "./types.js";
|
|
59
|
+
import { buildChatMessageContent } from "../prompts/message-utils.js";
|
|
55
60
|
|
|
56
61
|
// =============================================================================
|
|
57
62
|
// EMBEDDING STRIPPING - Remove embeddings from data items before returning to FE
|
|
@@ -120,6 +125,7 @@ export class Processor {
|
|
|
120
125
|
private currentRequest: LLMRequest | null = null;
|
|
121
126
|
private isTUI = false;
|
|
122
127
|
private lastOpenCodeSync = 0;
|
|
128
|
+
private lastDLQTrim = 0;
|
|
123
129
|
private openCodeImportInProgress = false;
|
|
124
130
|
private pendingConflict: StateConflictData | null = null;
|
|
125
131
|
|
|
@@ -228,7 +234,7 @@ export class Processor {
|
|
|
228
234
|
const welcomeMessage: Message = {
|
|
229
235
|
id: crypto.randomUUID(),
|
|
230
236
|
role: "system",
|
|
231
|
-
|
|
237
|
+
verbal_response: EI_WELCOME_MESSAGE,
|
|
232
238
|
timestamp: new Date().toISOString(),
|
|
233
239
|
read: false,
|
|
234
240
|
context_status: ContextStatusEnum.Always,
|
|
@@ -330,31 +336,36 @@ export class Processor {
|
|
|
330
336
|
await this.checkScheduledTasks();
|
|
331
337
|
|
|
332
338
|
if (this.queueProcessor.getState() === "idle") {
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
this.
|
|
339
|
+
const retryAfter = this.stateManager.queue_nextItemRetryAfter();
|
|
340
|
+
const isBackingOff = retryAfter !== null && retryAfter > new Date().toISOString();
|
|
341
|
+
|
|
342
|
+
if (!isBackingOff) {
|
|
343
|
+
const request = this.stateManager.queue_claimHighest();
|
|
344
|
+
if (request) {
|
|
345
|
+
const personaId = request.data.personaId as string | undefined;
|
|
346
|
+
const personaDisplayName = request.data.personaDisplayName as string | undefined;
|
|
347
|
+
const personaSuffix = personaDisplayName ? ` [${personaDisplayName}]` : "";
|
|
348
|
+
console.log(`[Processor ${this.instanceId}] processing request: ${request.next_step}${personaSuffix}`);
|
|
349
|
+
this.currentRequest = request;
|
|
350
|
+
|
|
351
|
+
if (personaId && request.next_step === LLMNextStep.HandlePersonaResponse) {
|
|
352
|
+
this.interface.onMessageProcessing?.(personaId);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
this.queueProcessor.start(request, async (response) => {
|
|
356
|
+
this.currentRequest = null;
|
|
357
|
+
await this.handleResponse(response);
|
|
358
|
+
const nextState = this.stateManager.queue_isPaused() ? "paused" : "idle";
|
|
359
|
+
// the processor state is set in the caller, so this needs a bit of delay
|
|
360
|
+
setTimeout(() => this.interface.onQueueStateChanged?.(nextState), 0);
|
|
361
|
+
}, {
|
|
362
|
+
accounts: this.stateManager.getHuman().settings?.accounts,
|
|
363
|
+
messageFetcher: (pName) => this.fetchMessagesForLLM(pName),
|
|
364
|
+
rawMessageFetcher: (pName) => this.stateManager.messages_get(pName),
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
this.interface.onQueueStateChanged?.("busy");
|
|
343
368
|
}
|
|
344
|
-
|
|
345
|
-
this.queueProcessor.start(request, async (response) => {
|
|
346
|
-
this.currentRequest = null;
|
|
347
|
-
await this.handleResponse(response);
|
|
348
|
-
const nextState = this.stateManager.queue_isPaused() ? "paused" : "idle";
|
|
349
|
-
// the processor state is set in the caller, so this needs a bit of delay
|
|
350
|
-
setTimeout(() => this.interface.onQueueStateChanged?.(nextState), 0);
|
|
351
|
-
}, {
|
|
352
|
-
accounts: this.stateManager.getHuman().settings?.accounts,
|
|
353
|
-
messageFetcher: (pName) => this.fetchMessagesForLLM(pName),
|
|
354
|
-
rawMessageFetcher: (pName) => this.stateManager.messages_get(pName),
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
this.interface.onQueueStateChanged?.("busy");
|
|
358
369
|
}
|
|
359
370
|
}
|
|
360
371
|
|
|
@@ -369,7 +380,7 @@ export class Processor {
|
|
|
369
380
|
|
|
370
381
|
const human = this.stateManager.getHuman();
|
|
371
382
|
|
|
372
|
-
if (this.isTUI && human.settings?.opencode?.integration) {
|
|
383
|
+
if (this.isTUI && human.settings?.opencode?.integration && this.stateManager.queue_length() === 0) {
|
|
373
384
|
await this.checkAndSyncOpenCode(human, now);
|
|
374
385
|
}
|
|
375
386
|
|
|
@@ -403,6 +414,15 @@ export class Processor {
|
|
|
403
414
|
}
|
|
404
415
|
}
|
|
405
416
|
}
|
|
417
|
+
// DLQ rolloff — once per day
|
|
418
|
+
const MS_PER_DAY = 86_400_000;
|
|
419
|
+
if (now - this.lastDLQTrim >= MS_PER_DAY) {
|
|
420
|
+
this.lastDLQTrim = now;
|
|
421
|
+
const trimmed = this.stateManager.queue_trimDLQ();
|
|
422
|
+
if (trimmed > 0) {
|
|
423
|
+
console.log(`[Processor] DLQ trim: removed ${trimmed} expired items`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
406
426
|
}
|
|
407
427
|
|
|
408
428
|
private async checkAndSyncOpenCode(human: HumanEntity, now: number): Promise<void> {
|
|
@@ -434,12 +454,10 @@ export class Processor {
|
|
|
434
454
|
},
|
|
435
455
|
});
|
|
436
456
|
|
|
437
|
-
const since = lastSync > 0 ? new Date(lastSync) : new Date(0);
|
|
438
|
-
|
|
439
457
|
this.openCodeImportInProgress = true;
|
|
440
458
|
import("../integrations/opencode/importer.js")
|
|
441
|
-
.then(({ importOpenCodeSessions }) =>
|
|
442
|
-
importOpenCodeSessions(
|
|
459
|
+
.then(({ importOpenCodeSessions }) =>
|
|
460
|
+
importOpenCodeSessions({
|
|
443
461
|
stateManager: this.stateManager,
|
|
444
462
|
interface: this.interface,
|
|
445
463
|
})
|
|
@@ -449,7 +467,7 @@ export class Processor {
|
|
|
449
467
|
console.log(
|
|
450
468
|
`[Processor] OpenCode sync complete: ${result.sessionsProcessed} sessions, ` +
|
|
451
469
|
`${result.topicsCreated} topics created, ${result.messagesImported} messages imported, ` +
|
|
452
|
-
`${result.
|
|
470
|
+
`${result.extractionScansQueued} extraction scans queued`
|
|
453
471
|
);
|
|
454
472
|
}
|
|
455
473
|
})
|
|
@@ -482,29 +500,36 @@ export class Processor {
|
|
|
482
500
|
contextWindowHours
|
|
483
501
|
);
|
|
484
502
|
|
|
485
|
-
return filteredHistory
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
503
|
+
return filteredHistory
|
|
504
|
+
.reduce<import("./types.js").ChatMessage[]>((acc, m) => {
|
|
505
|
+
const content = buildChatMessageContent(m);
|
|
506
|
+
if (content.length > 0) {
|
|
507
|
+
acc.push({
|
|
508
|
+
role: m.role === "human" ? "user" : "assistant",
|
|
509
|
+
content,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
return acc;
|
|
513
|
+
}, []);
|
|
489
514
|
}
|
|
490
515
|
|
|
491
516
|
private async queueHeartbeatCheck(personaId: string): Promise<void> {
|
|
492
517
|
const persona = this.stateManager.persona_getById(personaId);
|
|
493
518
|
if (!persona) return;
|
|
494
|
-
|
|
495
519
|
this.stateManager.persona_update(personaId, { last_heartbeat: new Date().toISOString() });
|
|
496
|
-
|
|
497
520
|
const human = this.stateManager.getHuman();
|
|
498
521
|
const history = this.stateManager.messages_get(personaId);
|
|
499
|
-
|
|
522
|
+
if (personaId === "ei") {
|
|
523
|
+
await this.queueEiHeartbeat(human, history);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
500
526
|
|
|
527
|
+
const filteredHuman = await this.filterHumanDataByVisibility(human, persona);
|
|
501
528
|
const inactiveDays = persona.last_activity
|
|
502
529
|
? Math.floor((Date.now() - new Date(persona.last_activity).getTime()) / (1000 * 60 * 60 * 24))
|
|
503
530
|
: 0;
|
|
504
|
-
|
|
505
531
|
const sortByEngagementGap = <T extends { exposure_desired: number; exposure_current: number }>(items: T[]): T[] =>
|
|
506
532
|
[...items].sort((a, b) => (b.exposure_desired - b.exposure_current) - (a.exposure_desired - a.exposure_current));
|
|
507
|
-
|
|
508
533
|
const promptData: HeartbeatCheckPromptData = {
|
|
509
534
|
persona: {
|
|
510
535
|
name: persona.display_name,
|
|
@@ -532,6 +557,118 @@ export class Processor {
|
|
|
532
557
|
});
|
|
533
558
|
}
|
|
534
559
|
|
|
560
|
+
private async queueEiHeartbeat(human: HumanEntity, history: import("./types.js").Message[]): Promise<void> {
|
|
561
|
+
const now = Date.now();
|
|
562
|
+
const engagementGapThreshold = 0.2;
|
|
563
|
+
const cooldownMs = 7 * 24 * 60 * 60 * 1000;
|
|
564
|
+
const personas = this.stateManager.persona_getAll();
|
|
565
|
+
const items: EiHeartbeatItem[] = [];
|
|
566
|
+
|
|
567
|
+
const unverifiedFacts = human.facts
|
|
568
|
+
.filter(f => f.validated === ValidationLevel.None && f.learned_by !== "Ei")
|
|
569
|
+
.slice(0, 5);
|
|
570
|
+
for (const fact of unverifiedFacts) {
|
|
571
|
+
const quote = human.quotes.find(q => q.data_item_ids.includes(fact.id));
|
|
572
|
+
items.push({
|
|
573
|
+
id: fact.id,
|
|
574
|
+
type: "Fact Check",
|
|
575
|
+
name: fact.name,
|
|
576
|
+
description: fact.description,
|
|
577
|
+
quote: quote?.text,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const underEngagedPeople = human.people
|
|
582
|
+
.filter(p =>
|
|
583
|
+
(p.exposure_desired - p.exposure_current) > engagementGapThreshold &&
|
|
584
|
+
(!p.last_ei_asked || now - new Date(p.last_ei_asked).getTime() > cooldownMs)
|
|
585
|
+
)
|
|
586
|
+
.sort((a, b) => (b.exposure_desired - b.exposure_current) - (a.exposure_desired - a.exposure_current))
|
|
587
|
+
.slice(0, 5);
|
|
588
|
+
for (const person of underEngagedPeople) {
|
|
589
|
+
const gap = Math.round((person.exposure_desired - person.exposure_current) * 100);
|
|
590
|
+
const quote = human.quotes.find(q => q.data_item_ids.includes(person.id));
|
|
591
|
+
items.push({
|
|
592
|
+
id: person.id,
|
|
593
|
+
type: "Low-Engagement Person",
|
|
594
|
+
engagement_delta: `${gap}%`,
|
|
595
|
+
relationship: person.relationship,
|
|
596
|
+
name: person.name,
|
|
597
|
+
description: person.description,
|
|
598
|
+
quote: quote?.text,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const underEngagedTopics = human.topics
|
|
603
|
+
.filter(t =>
|
|
604
|
+
(t.exposure_desired - t.exposure_current) > engagementGapThreshold &&
|
|
605
|
+
(!t.last_ei_asked || now - new Date(t.last_ei_asked).getTime() > cooldownMs)
|
|
606
|
+
)
|
|
607
|
+
.sort((a, b) => (b.exposure_desired - b.exposure_current) - (a.exposure_desired - a.exposure_current))
|
|
608
|
+
.slice(0, 5);
|
|
609
|
+
for (const topic of underEngagedTopics) {
|
|
610
|
+
const gap = Math.round((topic.exposure_desired - topic.exposure_current) * 100);
|
|
611
|
+
const quote = human.quotes.find(q => q.data_item_ids.includes(topic.id));
|
|
612
|
+
items.push({
|
|
613
|
+
id: topic.id,
|
|
614
|
+
type: "Low-Engagement Topic",
|
|
615
|
+
engagement_delta: `${gap}%`,
|
|
616
|
+
name: topic.name,
|
|
617
|
+
description: topic.description,
|
|
618
|
+
quote: quote?.text,
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const activePersonas = personas
|
|
623
|
+
.filter(p => !p.is_archived && !p.is_paused && p.id !== "ei")
|
|
624
|
+
.map(p => {
|
|
625
|
+
const msgs = this.stateManager.messages_get(p.id);
|
|
626
|
+
const lastHuman = [...msgs].reverse().find(m => m.role === "human");
|
|
627
|
+
const lastTs = lastHuman?.timestamp ? new Date(lastHuman.timestamp).getTime() : 0;
|
|
628
|
+
return { persona: p, lastHumanTs: lastTs };
|
|
629
|
+
})
|
|
630
|
+
.filter(({ lastHumanTs }) => {
|
|
631
|
+
const daysSince = (now - lastHumanTs) / (1000 * 60 * 60 * 24);
|
|
632
|
+
return daysSince >= 3;
|
|
633
|
+
})
|
|
634
|
+
.sort((a, b) => a.lastHumanTs - b.lastHumanTs)
|
|
635
|
+
.slice(0, 3);
|
|
636
|
+
for (const { persona: p, lastHumanTs } of activePersonas) {
|
|
637
|
+
const daysSince = lastHumanTs > 0
|
|
638
|
+
? Math.floor((now - lastHumanTs) / (1000 * 60 * 60 * 24))
|
|
639
|
+
: 999;
|
|
640
|
+
items.push({
|
|
641
|
+
id: p.id,
|
|
642
|
+
type: "Inactive Persona",
|
|
643
|
+
name: p.display_name,
|
|
644
|
+
short_description: p.short_description,
|
|
645
|
+
days_inactive: daysSince,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (items.length === 0) {
|
|
650
|
+
console.log("[queueEiHeartbeat] No items to address, skipping");
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const promptData: EiHeartbeatPromptData = {
|
|
655
|
+
items,
|
|
656
|
+
recent_history: history.slice(-10),
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const prompt = buildEiHeartbeatPrompt(promptData);
|
|
660
|
+
|
|
661
|
+
this.stateManager.queue_enqueue({
|
|
662
|
+
type: LLMRequestType.JSON,
|
|
663
|
+
priority: LLMPriority.Low,
|
|
664
|
+
system: prompt.system,
|
|
665
|
+
user: prompt.user,
|
|
666
|
+
next_step: LLMNextStep.HandleEiHeartbeat,
|
|
667
|
+
model: this.getModelForPersona("ei"),
|
|
668
|
+
data: { personaId: "ei", isTUI: this.isTUI },
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
535
672
|
|
|
536
673
|
private classifyLLMError(error: string): string {
|
|
537
674
|
const match = error.match(/\((\d{3})\)/);
|
|
@@ -627,9 +764,7 @@ export class Processor {
|
|
|
627
764
|
}
|
|
628
765
|
}
|
|
629
766
|
|
|
630
|
-
|
|
631
|
-
this.interface.onHumanUpdated?.();
|
|
632
|
-
}
|
|
767
|
+
|
|
633
768
|
|
|
634
769
|
if (response.request.next_step === LLMNextStep.HandleHumanItemUpdate) {
|
|
635
770
|
this.interface.onHumanUpdated?.();
|
|
@@ -813,23 +948,16 @@ export class Processor {
|
|
|
813
948
|
async recallPendingMessages(personaId: string): Promise<string> {
|
|
814
949
|
const persona = this.stateManager.persona_getById(personaId);
|
|
815
950
|
if (!persona) return "";
|
|
816
|
-
|
|
817
951
|
this.clearPendingRequestsFor(personaId);
|
|
818
|
-
this.stateManager.queue_pause();
|
|
819
|
-
|
|
820
952
|
const messages = this.stateManager.messages_get(personaId);
|
|
821
953
|
const pendingIds = messages
|
|
822
954
|
.filter(m => m.role === "human" && !m.read)
|
|
823
955
|
.map(m => m.id);
|
|
824
|
-
|
|
825
956
|
if (pendingIds.length === 0) return "";
|
|
826
|
-
|
|
827
957
|
const removed = this.stateManager.messages_remove(personaId, pendingIds);
|
|
828
|
-
const recalledContent = removed.map(m => m.
|
|
829
|
-
|
|
958
|
+
const recalledContent = removed.map(m => m.verbal_response ?? '').join("\n\n");
|
|
830
959
|
this.interface.onMessageAdded?.(personaId);
|
|
831
960
|
this.interface.onMessageRecalled?.(personaId, recalledContent);
|
|
832
|
-
|
|
833
961
|
return recalledContent;
|
|
834
962
|
}
|
|
835
963
|
|
|
@@ -848,7 +976,7 @@ export class Processor {
|
|
|
848
976
|
const message: Message = {
|
|
849
977
|
id: crypto.randomUUID(),
|
|
850
978
|
role: "human",
|
|
851
|
-
content,
|
|
979
|
+
verbal_response: content,
|
|
852
980
|
timestamp: new Date().toISOString(),
|
|
853
981
|
read: false,
|
|
854
982
|
context_status: "default" as ContextStatus,
|
|
@@ -987,13 +1115,40 @@ export class Processor {
|
|
|
987
1115
|
): Promise<ResponsePromptData["human"]> {
|
|
988
1116
|
const DEFAULT_GROUP = "General";
|
|
989
1117
|
const QUOTE_LIMIT = 10;
|
|
1118
|
+
const DATA_ITEM_LIMIT = 15;
|
|
990
1119
|
const SIMILARITY_THRESHOLD = 0.3;
|
|
1120
|
+
// Generic relevance selector for embedding-capable items.
|
|
1121
|
+
// Falls back to returning all items when no message/embeddings are available.
|
|
1122
|
+
const selectRelevantItems = async <T extends { id: string; embedding?: number[] }>(
|
|
1123
|
+
items: T[],
|
|
1124
|
+
limit: number
|
|
1125
|
+
): Promise<T[]> => {
|
|
1126
|
+
if (items.length === 0) return [];
|
|
1127
|
+
|
|
1128
|
+
const withEmbeddings = items.filter(i => i.embedding?.length);
|
|
1129
|
+
|
|
1130
|
+
if (currentMessage && withEmbeddings.length > 0) {
|
|
1131
|
+
try {
|
|
1132
|
+
const embeddingService = getEmbeddingService();
|
|
1133
|
+
const queryVector = await embeddingService.embed(currentMessage);
|
|
1134
|
+
const results = findTopK(queryVector, withEmbeddings, limit);
|
|
1135
|
+
const relevant = results
|
|
1136
|
+
.filter(({ similarity }) => similarity >= SIMILARITY_THRESHOLD)
|
|
1137
|
+
.map(({ item }) => item);
|
|
1138
|
+
|
|
1139
|
+
if (relevant.length > 0) return relevant;
|
|
1140
|
+
} catch (err) {
|
|
1141
|
+
console.warn("[filterHumanDataByVisibility] Embedding search failed:", err);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
991
1144
|
|
|
1145
|
+
// Fallback: return all items (caller may apply its own limit)
|
|
1146
|
+
return items;
|
|
1147
|
+
};
|
|
992
1148
|
const selectRelevantQuotes = async (quotes: Quote[]): Promise<Quote[]> => {
|
|
993
1149
|
if (quotes.length === 0) return [];
|
|
994
|
-
|
|
995
1150
|
const withEmbeddings = quotes.filter(q => q.embedding?.length);
|
|
996
|
-
|
|
1151
|
+
|
|
997
1152
|
if (currentMessage && withEmbeddings.length > 0) {
|
|
998
1153
|
try {
|
|
999
1154
|
const embeddingService = getEmbeddingService();
|
|
@@ -1002,35 +1157,31 @@ export class Processor {
|
|
|
1002
1157
|
const relevant = results
|
|
1003
1158
|
.filter(({ similarity }) => similarity >= SIMILARITY_THRESHOLD)
|
|
1004
1159
|
.map(({ item }) => item);
|
|
1005
|
-
|
|
1160
|
+
|
|
1006
1161
|
if (relevant.length > 0) return relevant;
|
|
1007
1162
|
} catch (err) {
|
|
1008
1163
|
console.warn("[filterHumanDataByVisibility] Embedding search failed:", err);
|
|
1009
1164
|
}
|
|
1010
1165
|
}
|
|
1011
|
-
|
|
1012
1166
|
return [...quotes]
|
|
1013
1167
|
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
|
1014
1168
|
.slice(0, QUOTE_LIMIT);
|
|
1015
1169
|
};
|
|
1016
|
-
|
|
1017
1170
|
if (persona.id === "ei") {
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
};
|
|
1171
|
+
const [facts, traits, topics, people, quotes] = await Promise.all([
|
|
1172
|
+
selectRelevantItems(human.facts, DATA_ITEM_LIMIT),
|
|
1173
|
+
selectRelevantItems(human.traits, DATA_ITEM_LIMIT),
|
|
1174
|
+
selectRelevantItems(human.topics, DATA_ITEM_LIMIT),
|
|
1175
|
+
selectRelevantItems(human.people, DATA_ITEM_LIMIT),
|
|
1176
|
+
selectRelevantQuotes(human.quotes ?? []),
|
|
1177
|
+
]);
|
|
1178
|
+
return { facts, traits, topics, people, quotes };
|
|
1026
1179
|
}
|
|
1027
|
-
|
|
1028
1180
|
const visibleGroups = new Set<string>();
|
|
1029
1181
|
if (persona.group_primary) {
|
|
1030
1182
|
visibleGroups.add(persona.group_primary);
|
|
1031
1183
|
}
|
|
1032
1184
|
(persona.groups_visible ?? []).forEach((g) => visibleGroups.add(g));
|
|
1033
|
-
|
|
1034
1185
|
const filterByGroup = <T extends DataItemBase>(items: T[]): T[] => {
|
|
1035
1186
|
return items.filter((item) => {
|
|
1036
1187
|
const itemGroups = item.persona_groups ?? [];
|
|
@@ -1038,21 +1189,20 @@ export class Processor {
|
|
|
1038
1189
|
return effectiveGroups.some((g) => visibleGroups.has(g));
|
|
1039
1190
|
});
|
|
1040
1191
|
};
|
|
1041
|
-
|
|
1042
1192
|
const groupFilteredQuotes = (human.quotes ?? []).filter((q) => {
|
|
1043
1193
|
const effectiveGroups = q.persona_groups.length === 0 ? [DEFAULT_GROUP] : q.persona_groups;
|
|
1044
1194
|
return effectiveGroups.some((g) => visibleGroups.has(g));
|
|
1045
1195
|
});
|
|
1046
1196
|
|
|
1047
|
-
const
|
|
1197
|
+
const [facts, traits, topics, people, quotes] = await Promise.all([
|
|
1198
|
+
selectRelevantItems(filterByGroup(human.facts), DATA_ITEM_LIMIT),
|
|
1199
|
+
selectRelevantItems(filterByGroup(human.traits), DATA_ITEM_LIMIT),
|
|
1200
|
+
selectRelevantItems(filterByGroup(human.topics), DATA_ITEM_LIMIT),
|
|
1201
|
+
selectRelevantItems(filterByGroup(human.people), DATA_ITEM_LIMIT),
|
|
1202
|
+
selectRelevantQuotes(groupFilteredQuotes),
|
|
1203
|
+
]);
|
|
1048
1204
|
|
|
1049
|
-
return {
|
|
1050
|
-
facts: filterByGroup(human.facts),
|
|
1051
|
-
traits: filterByGroup(human.traits),
|
|
1052
|
-
topics: filterByGroup(human.topics),
|
|
1053
|
-
people: filterByGroup(human.people),
|
|
1054
|
-
quotes: relevantQuotes,
|
|
1055
|
-
};
|
|
1205
|
+
return { facts, traits, topics, people, quotes };
|
|
1056
1206
|
}
|
|
1057
1207
|
|
|
1058
1208
|
private getVisiblePersonas(
|
|
@@ -1342,13 +1492,31 @@ export class Processor {
|
|
|
1342
1492
|
return {
|
|
1343
1493
|
state: this.stateManager.queue_isPaused()
|
|
1344
1494
|
? "paused"
|
|
1345
|
-
: this.
|
|
1495
|
+
: this.stateManager.queue_hasProcessingItem()
|
|
1346
1496
|
? "busy"
|
|
1347
1497
|
: "idle",
|
|
1348
1498
|
pending_count: this.stateManager.queue_length(),
|
|
1499
|
+
dlq_count: this.stateManager.queue_dlqLength(),
|
|
1349
1500
|
};
|
|
1350
1501
|
}
|
|
1351
1502
|
|
|
1503
|
+
pauseQueue(): void {
|
|
1504
|
+
this.stateManager.queue_pause();
|
|
1505
|
+
this.queueProcessor.abort();
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
getQueueActiveItems(): LLMRequest[] {
|
|
1509
|
+
return this.stateManager.queue_getAllActiveItems();
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
getDLQItems(): LLMRequest[] {
|
|
1513
|
+
return this.stateManager.queue_getDLQItems();
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
updateQueueItem(id: string, updates: Partial<LLMRequest>): boolean {
|
|
1517
|
+
return this.stateManager.queue_updateItem(id, updates);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1352
1520
|
async clearQueue(): Promise<number> {
|
|
1353
1521
|
this.queueProcessor.abort();
|
|
1354
1522
|
return this.stateManager.queue_clear();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { callLLMRaw, parseJSONResponse
|
|
1
|
+
import { LLMRequest, LLMResponse, LLMRequestType, ProviderAccount, ChatMessage, Message } from "./types.js";
|
|
2
|
+
import { callLLMRaw, parseJSONResponse } from "./llm-client.js";
|
|
3
3
|
import { hydratePromptPlaceholders } from "../prompts/message-utils.js";
|
|
4
4
|
|
|
5
5
|
type QueueProcessorState = "idle" | "busy";
|
|
@@ -133,9 +133,8 @@ export class QueueProcessor {
|
|
|
133
133
|
): LLMResponse {
|
|
134
134
|
switch (request.type) {
|
|
135
135
|
case "json" as LLMRequestType:
|
|
136
|
-
return this.handleJSONResponse(request, content, finishReason);
|
|
137
136
|
case "response" as LLMRequestType:
|
|
138
|
-
return this.
|
|
137
|
+
return this.handleJSONResponse(request, content, finishReason);
|
|
139
138
|
case "raw" as LLMRequestType:
|
|
140
139
|
default:
|
|
141
140
|
return {
|
|
@@ -172,26 +171,4 @@ export class QueueProcessor {
|
|
|
172
171
|
}
|
|
173
172
|
}
|
|
174
173
|
|
|
175
|
-
private handleConversationResponse(
|
|
176
|
-
request: LLMRequest,
|
|
177
|
-
content: string,
|
|
178
|
-
finishReason: string | null
|
|
179
|
-
): LLMResponse {
|
|
180
|
-
const cleaned = cleanResponseContent(content);
|
|
181
|
-
|
|
182
|
-
const noMessagePatterns = [
|
|
183
|
-
/^no\s*(new\s*)?(message|response)/i,
|
|
184
|
-
/^nothing\s+to\s+(say|add)/i,
|
|
185
|
-
/^\[no\s+message\]/i,
|
|
186
|
-
];
|
|
187
|
-
|
|
188
|
-
const isNoMessage = noMessagePatterns.some((p) => p.test(cleaned));
|
|
189
|
-
|
|
190
|
-
return {
|
|
191
|
-
request,
|
|
192
|
-
success: true,
|
|
193
|
-
content: isNoMessage ? null : cleaned,
|
|
194
|
-
finish_reason: finishReason ?? undefined,
|
|
195
|
-
};
|
|
196
174
|
}
|
|
197
|
-
}
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import type { PersonaEntity, Message, ContextStatus } from "../types.js";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Migration: If a persisted message has the old `content` field but no `verbal_response`,
|
|
5
|
+
* move content → verbal_response. Runs on every load (no-op for already-migrated data).
|
|
6
|
+
*/
|
|
7
|
+
function migrateMessage(msg: Message & { content?: string }): Message {
|
|
8
|
+
if (msg.content !== undefined && msg.verbal_response === undefined) {
|
|
9
|
+
const { content, ...rest } = msg;
|
|
10
|
+
return { ...rest, verbal_response: content };
|
|
11
|
+
}
|
|
12
|
+
return msg;
|
|
13
|
+
}
|
|
14
|
+
|
|
3
15
|
export interface PersonaData {
|
|
4
16
|
entity: PersonaEntity;
|
|
5
17
|
messages: Message[];
|
|
@@ -13,7 +25,7 @@ export class PersonaState {
|
|
|
13
25
|
this.personas = new Map(
|
|
14
26
|
Object.entries(personas).map(([id, data]) => [
|
|
15
27
|
id,
|
|
16
|
-
{ entity: data.entity, messages: data.messages },
|
|
28
|
+
{ entity: data.entity, messages: data.messages.map(migrateMessage) },
|
|
17
29
|
])
|
|
18
30
|
);
|
|
19
31
|
}
|