ei-tui 1.6.0 → 1.6.2
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 +21 -6
- package/package.json +1 -1
- package/src/cli/README.md +9 -7
- package/src/cli/mcp.ts +3 -3
- package/src/cli/retrieval.ts +22 -0
- package/src/cli.ts +213 -13
- package/src/core/context-utils.ts +0 -1
- package/src/core/orchestrators/ceremony.ts +48 -17
- package/src/core/processor.ts +92 -3
- package/src/core/prompt-context-builder.ts +2 -0
- package/src/core/tools/builtin/persona-notes.ts +81 -0
- package/src/core/tools/index.ts +56 -0
- package/src/core/types/data-items.ts +1 -1
- package/src/core/types/entities.ts +2 -0
- package/src/core/types/llm.ts +1 -1
- package/src/core/utils/message-id.ts +16 -0
- package/src/integrations/codex/importer.ts +258 -0
- package/src/integrations/codex/index.ts +11 -0
- package/src/integrations/codex/reader.ts +241 -0
- package/src/integrations/codex/types.ts +117 -0
- package/src/integrations/opencode/reader-factory.ts +4 -4
- package/src/integrations/slack/importer.ts +0 -1
- package/src/prompts/response/index.ts +5 -2
- package/src/prompts/response/sections.ts +10 -0
- package/src/prompts/response/types.ts +1 -0
- package/src/prompts/room/index.ts +3 -0
- package/src/prompts/room/types.ts +1 -0
- package/tui/README.md +4 -3
- package/tui/src/util/yaml-settings.ts +28 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LLMRequestType, LLMPriority, LLMNextStep, RoomMode, ContextStatus, type CeremonyConfig, type PersonaTopic, type Topic } from "../types.js";
|
|
1
|
+
import { LLMRequestType, LLMPriority, LLMNextStep, RoomMode, ContextStatus, type CeremonyConfig, type PersonaTopic, type Topic, type Message } from "../types.js";
|
|
2
2
|
import type { StateManager } from "../state-manager.js";
|
|
3
3
|
import { normalizeRoomMessages } from "../handlers/utils.js";
|
|
4
4
|
import { applyDecayToValue } from "../utils/index.js";
|
|
@@ -168,9 +168,9 @@ function queueExposurePhase(personaId: string, state: StateManager, options?: Ex
|
|
|
168
168
|
* If any ceremony_progress items remain in the queue, does nothing — more work pending.
|
|
169
169
|
* Phase 1: Dedup → Phase 2: Expose → Phase 3: EventSummary → Decay → Phase 4: Person Rewrite → Topic Rewrite (fire-and-forget)
|
|
170
170
|
*/
|
|
171
|
-
export function handleCeremonyProgress(state: StateManager, lastPhase: number):
|
|
171
|
+
export function handleCeremonyProgress(state: StateManager, lastPhase: number): { wroteEiWarning: boolean } {
|
|
172
172
|
if (state.queue_hasPendingCeremonies()) {
|
|
173
|
-
return; // Still processing ceremony items
|
|
173
|
+
return { wroteEiWarning: false }; // Still processing ceremony items
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
if (lastPhase === 1) {
|
|
@@ -232,13 +232,13 @@ export function handleCeremonyProgress(state: StateManager, lastPhase: number):
|
|
|
232
232
|
console.log(`[ceremony:expose] Queued room persona topic rating: ${personaForRoom.display_name} in "${room.display_name}" (${unprocessedRaw.length} messages)`);
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
|
-
return;
|
|
235
|
+
return { wroteEiWarning: false };
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
if (lastPhase === 4) {
|
|
239
239
|
console.log("[ceremony:progress] Person Rewrite complete, starting Topic Rewrite");
|
|
240
240
|
queueTopicRewritePhase(state);
|
|
241
|
-
return;
|
|
241
|
+
return { wroteEiWarning: false };
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
if (lastPhase === 2) {
|
|
@@ -251,7 +251,7 @@ export function handleCeremonyProgress(state: StateManager, lastPhase: number):
|
|
|
251
251
|
console.log("[ceremony:progress] No event summary work, advancing to Decay");
|
|
252
252
|
handleCeremonyProgress(state, 3);
|
|
253
253
|
}
|
|
254
|
-
return;
|
|
254
|
+
return { wroteEiWarning: false };
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
// Phase 3 (EventSummary) complete → advance to Decay/Prune then Person Rewrite (phase 4)
|
|
@@ -285,9 +285,10 @@ export function handleCeremonyProgress(state: StateManager, lastPhase: number):
|
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
// Reflection phase: fire-and-forget critic calls for persona person records above threshold
|
|
288
|
-
queueReflectionPhase(state);
|
|
288
|
+
const wroteEiWarning = queueReflectionPhase(state);
|
|
289
289
|
|
|
290
290
|
console.log("[ceremony:progress] Ceremony Decay complete");
|
|
291
|
+
return { wroteEiWarning };
|
|
291
292
|
}
|
|
292
293
|
|
|
293
294
|
// =============================================================================
|
|
@@ -446,7 +447,8 @@ export function runHumanCeremony(state: StateManager): void {
|
|
|
446
447
|
// REWRITE PHASE (fire-and-forget — queues Low-priority Phase 1 scans)
|
|
447
448
|
// =============================================================================
|
|
448
449
|
|
|
449
|
-
const
|
|
450
|
+
const PERSON_REWRITE_DESCRIPTION_THRESHOLD = 1000;
|
|
451
|
+
const TOPIC_REWRITE_DESCRIPTION_THRESHOLD = 750;
|
|
450
452
|
|
|
451
453
|
/**
|
|
452
454
|
* Forces an unconditional, threshold-bypassing Person scan on Apply/Dismiss.
|
|
@@ -473,7 +475,7 @@ export function queueReflectionDrain(personaId: string, state: StateManager): vo
|
|
|
473
475
|
messages_analyze: unextractedPeople,
|
|
474
476
|
extraction_flag: "p",
|
|
475
477
|
};
|
|
476
|
-
queuePersonScan(context, state);
|
|
478
|
+
queuePersonScan(context, state, { reflection_progress: 1 });
|
|
477
479
|
console.log(`[reflection:drain] Queued Person scan for ${persona.display_name} (${unextractedPeople.length} messages) — clears on completion`);
|
|
478
480
|
}
|
|
479
481
|
|
|
@@ -494,7 +496,7 @@ export function queuePersonRewritePhase(state: StateManager, options?: { ceremon
|
|
|
494
496
|
i => i.type.toLowerCase() === 'ei persona'
|
|
495
497
|
);
|
|
496
498
|
return !isPersonaLinked
|
|
497
|
-
&& (person.description?.length ?? 0) >
|
|
499
|
+
&& (person.description?.length ?? 0) > PERSON_REWRITE_DESCRIPTION_THRESHOLD;
|
|
498
500
|
});
|
|
499
501
|
|
|
500
502
|
const alreadyChecked = allCandidates.filter(p => {
|
|
@@ -520,7 +522,7 @@ export function queuePersonRewritePhase(state: StateManager, options?: { ceremon
|
|
|
520
522
|
return;
|
|
521
523
|
}
|
|
522
524
|
|
|
523
|
-
console.log(`[ceremony:rewrite] Found ${personsToScan.length} person(s) above ${
|
|
525
|
+
console.log(`[ceremony:rewrite] Found ${personsToScan.length} person(s) above ${PERSON_REWRITE_DESCRIPTION_THRESHOLD} chars — queueing person rewrite scans`);
|
|
524
526
|
|
|
525
527
|
for (const person of personsToScan) {
|
|
526
528
|
const prompt = buildPersonRewriteScanPrompt({ item: person, itemType: "person" });
|
|
@@ -552,7 +554,7 @@ export function queueTopicRewritePhase(state: StateManager): void {
|
|
|
552
554
|
|
|
553
555
|
const human = state.getHuman();
|
|
554
556
|
const allCandidateTopics = human.topics.filter(topic =>
|
|
555
|
-
(topic.description?.length ?? 0) >
|
|
557
|
+
(topic.description?.length ?? 0) > TOPIC_REWRITE_DESCRIPTION_THRESHOLD
|
|
556
558
|
);
|
|
557
559
|
|
|
558
560
|
const alreadyCheckedTopics = allCandidateTopics.filter(t => {
|
|
@@ -578,7 +580,7 @@ export function queueTopicRewritePhase(state: StateManager): void {
|
|
|
578
580
|
return;
|
|
579
581
|
}
|
|
580
582
|
|
|
581
|
-
console.log(`[ceremony:rewrite] Found ${topicsToScan.length} topic(s) above ${
|
|
583
|
+
console.log(`[ceremony:rewrite] Found ${topicsToScan.length} topic(s) above ${TOPIC_REWRITE_DESCRIPTION_THRESHOLD} chars — queueing topic rewrite scans`);
|
|
582
584
|
|
|
583
585
|
for (const topic of topicsToScan) {
|
|
584
586
|
const prompt = buildTopicRewriteScanPrompt({ item: topic, itemType: "topic" });
|
|
@@ -615,16 +617,43 @@ function queueEventSummaryForAll(state: StateManager, options?: ExtractionOption
|
|
|
615
617
|
console.log(`[ceremony:event] Queued event summary scans for ${activePersonas.length} personas (${totalQueued} total chunks)`);
|
|
616
618
|
}
|
|
617
619
|
|
|
618
|
-
function queueReflectionPhase(state: StateManager):
|
|
620
|
+
function queueReflectionPhase(state: StateManager): boolean {
|
|
619
621
|
const personas = state.persona_getAll().filter(p =>
|
|
620
622
|
!p.is_paused && !p.is_archived && !p.is_static
|
|
621
623
|
);
|
|
622
624
|
|
|
625
|
+
const human = state.getHuman();
|
|
623
626
|
let queued = 0;
|
|
627
|
+
let wroteEiWarning = false;
|
|
628
|
+
|
|
624
629
|
for (const persona of personas) {
|
|
625
|
-
const
|
|
626
|
-
|
|
630
|
+
const linkedRecords = human.people.filter(p =>
|
|
631
|
+
p.identifiers?.some(i => i.type.toLowerCase() === 'ei persona' && i.value === persona.id)
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
if (linkedRecords.length === 0) continue;
|
|
635
|
+
|
|
636
|
+
const overThreshold = linkedRecords.filter(p => (p.description?.length ?? 0) > PERSON_LOG_REFLECTION_THRESHOLD);
|
|
637
|
+
if (overThreshold.length === 0) continue;
|
|
638
|
+
|
|
639
|
+
if (linkedRecords.length > 1) {
|
|
640
|
+
const names = linkedRecords.map(p => `"${p.name}"`).join(" and ");
|
|
641
|
+
console.log(`[ceremony:reflection] ${persona.display_name} is linked to multiple person records (${names}) — skipping reflection, writing Ei warning`);
|
|
642
|
+
|
|
643
|
+
const warning: Message = {
|
|
644
|
+
id: crypto.randomUUID(),
|
|
645
|
+
role: "system",
|
|
646
|
+
content: `During today's ceremony, I noticed that **${persona.display_name}** is connected to multiple person records: ${names}. This might be intentional — if you created a composite persona — but if not, you may want to check the identifiers on those records. Reflection for ${persona.display_name} has been paused until this is resolved.`,
|
|
647
|
+
timestamp: new Date().toISOString(),
|
|
648
|
+
read: false,
|
|
649
|
+
context_status: ContextStatus.Always,
|
|
650
|
+
};
|
|
651
|
+
state.messages_append("ei", warning);
|
|
652
|
+
wroteEiWarning = true;
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
627
655
|
|
|
656
|
+
const personRecord = linkedRecords[0];
|
|
628
657
|
const prompt = buildReflectionCriticPrompt({
|
|
629
658
|
persona_identity: {
|
|
630
659
|
name: persona.display_name,
|
|
@@ -650,7 +679,9 @@ function queueReflectionPhase(state: StateManager): void {
|
|
|
650
679
|
console.log(`[ceremony:reflection] Queued critic for ${persona.display_name} (person log: ${personRecord.description?.length} chars)`);
|
|
651
680
|
}
|
|
652
681
|
|
|
653
|
-
if (queued === 0) {
|
|
682
|
+
if (queued === 0 && !wroteEiWarning) {
|
|
654
683
|
console.log("[ceremony:reflection] No persona person records above threshold — skipping");
|
|
655
684
|
}
|
|
685
|
+
|
|
686
|
+
return wroteEiWarning;
|
|
656
687
|
}
|
package/src/core/processor.ts
CHANGED
|
@@ -36,7 +36,8 @@ import { handlers } from "./handlers/index.js";
|
|
|
36
36
|
import { normalizeRoomMessages, getMessageContent } from "./handlers/utils.js";
|
|
37
37
|
import { sanitizeEiPersonaIdentifiers } from "./utils/identifier-utils.js";
|
|
38
38
|
import { ContextStatus as ContextStatusEnum, RoomMode } from "./types.js";
|
|
39
|
-
import { registerFindMemoryExecutor, registerFetchMemoryExecutor, registerFetchMessageExecutor, registerFileReadExecutor, SYSTEM_TOOLS } from "./tools/index.js";
|
|
39
|
+
import { registerFindMemoryExecutor, registerFetchMemoryExecutor, registerFetchMessageExecutor, registerFileReadExecutor, registerPersonaNoteExecutors, buildPersonaNoteTools, SYSTEM_TOOLS } from "./tools/index.js";
|
|
40
|
+
import { createAddNoteExecutor, createClearNoteExecutor } from "./tools/builtin/persona-notes.js";
|
|
40
41
|
import { createFindMemoryExecutor } from "./tools/builtin/find-memory.js";
|
|
41
42
|
import { createFetchMemoryExecutor } from "./tools/builtin/fetch-memory.js";
|
|
42
43
|
import { createFetchMessageExecutor } from "./tools/builtin/fetch-message.js";
|
|
@@ -147,6 +148,7 @@ const DEFAULT_LOOP_INTERVAL_MS = 100;
|
|
|
147
148
|
const DEFAULT_OPENCODE_POLLING_MS = 60000;
|
|
148
149
|
const DEFAULT_CLAUDE_CODE_POLLING_MS = 60000;
|
|
149
150
|
const DEFAULT_CURSOR_POLLING_MS = 60000;
|
|
151
|
+
const DEFAULT_CODEX_POLLING_MS = 60000;
|
|
150
152
|
|
|
151
153
|
let processorInstanceCount = 0;
|
|
152
154
|
|
|
@@ -169,6 +171,8 @@ export class Processor {
|
|
|
169
171
|
private claudeCodeImportInProgress = false;
|
|
170
172
|
private lastCursorSync = 0;
|
|
171
173
|
private cursorImportInProgress = false;
|
|
174
|
+
private lastCodexSync = 0;
|
|
175
|
+
private codexImportInProgress = false;
|
|
172
176
|
private lastSlackSync = 0;
|
|
173
177
|
private slackImportInProgress = false;
|
|
174
178
|
private pendingConflict: StateConflictData | null = null;
|
|
@@ -252,6 +256,10 @@ export class Processor {
|
|
|
252
256
|
this.seedSettings();
|
|
253
257
|
registerFindMemoryExecutor(createFindMemoryExecutor(this.searchHumanData.bind(this), this.getPersonaList.bind(this), this.stateManager.getHuman.bind(this.stateManager)));
|
|
254
258
|
registerFetchMemoryExecutor(createFetchMemoryExecutor(this.stateManager.getHuman.bind(this.stateManager)));
|
|
259
|
+
registerPersonaNoteExecutors(
|
|
260
|
+
createAddNoteExecutor(this.stateManager.persona_getById.bind(this.stateManager), this.stateManager.persona_update.bind(this.stateManager)),
|
|
261
|
+
createClearNoteExecutor(this.stateManager.persona_getById.bind(this.stateManager), this.stateManager.persona_update.bind(this.stateManager))
|
|
262
|
+
);
|
|
255
263
|
if (this.isTUI) {
|
|
256
264
|
await registerFileReadExecutor();
|
|
257
265
|
const retrievalPath = "../cli/retrieval.js";
|
|
@@ -1195,6 +1203,14 @@ export class Processor {
|
|
|
1195
1203
|
modified = true;
|
|
1196
1204
|
}
|
|
1197
1205
|
|
|
1206
|
+
if (!human.settings.codex) {
|
|
1207
|
+
human.settings.codex = {
|
|
1208
|
+
integration: false,
|
|
1209
|
+
polling_interval_ms: 60000,
|
|
1210
|
+
};
|
|
1211
|
+
modified = true;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1198
1214
|
if (!human.settings.ceremony) {
|
|
1199
1215
|
human.settings.ceremony = {
|
|
1200
1216
|
time: "09:00",
|
|
@@ -1272,6 +1288,14 @@ export class Processor {
|
|
|
1272
1288
|
console.log(`[Processor ${this.instanceId}] Clearing claudeCodeImportInProgress flag`);
|
|
1273
1289
|
this.claudeCodeImportInProgress = false;
|
|
1274
1290
|
}
|
|
1291
|
+
if (this.cursorImportInProgress) {
|
|
1292
|
+
console.log(`[Processor ${this.instanceId}] Clearing cursorImportInProgress flag`);
|
|
1293
|
+
this.cursorImportInProgress = false;
|
|
1294
|
+
}
|
|
1295
|
+
if (this.codexImportInProgress) {
|
|
1296
|
+
console.log(`[Processor ${this.instanceId}] Clearing codexImportInProgress flag`);
|
|
1297
|
+
this.codexImportInProgress = false;
|
|
1298
|
+
}
|
|
1275
1299
|
if (this.slackImportInProgress) {
|
|
1276
1300
|
console.log(`[Processor ${this.instanceId}] Clearing slackImportInProgress flag`);
|
|
1277
1301
|
this.slackImportInProgress = false;
|
|
@@ -1414,7 +1438,7 @@ const toolNextSteps = new Set([
|
|
|
1414
1438
|
t.name === "find_memory" || t.name === "fetch_memory" || t.name === "fetch_message"
|
|
1415
1439
|
);
|
|
1416
1440
|
} else if (toolNextSteps.has(request.next_step) && toolPersonaId) {
|
|
1417
|
-
tools = [...SYSTEM_TOOLS, ...this.stateManager.tools_getForPersona(toolPersonaId, this.isTUI)];
|
|
1441
|
+
tools = [...SYSTEM_TOOLS, ...buildPersonaNoteTools(toolPersonaId), ...this.stateManager.tools_getForPersona(toolPersonaId, this.isTUI)];
|
|
1418
1442
|
}
|
|
1419
1443
|
|
|
1420
1444
|
// Auto-inject each handler's dedicated submit tool — infrastructure, not user-visible.
|
|
@@ -1517,6 +1541,14 @@ const toolNextSteps = new Set([
|
|
|
1517
1541
|
await this.checkAndSyncCursor(human, now);
|
|
1518
1542
|
}
|
|
1519
1543
|
|
|
1544
|
+
if (
|
|
1545
|
+
this.isTUI &&
|
|
1546
|
+
human.settings?.codex?.integration &&
|
|
1547
|
+
this.stateManager.queue_length() === 0
|
|
1548
|
+
) {
|
|
1549
|
+
await this.checkAndSyncCodex(human, now);
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1520
1552
|
if (
|
|
1521
1553
|
this.isTUI &&
|
|
1522
1554
|
human.settings?.personaHistory?.integration &&
|
|
@@ -1774,6 +1806,60 @@ const toolNextSteps = new Set([
|
|
|
1774
1806
|
});
|
|
1775
1807
|
}
|
|
1776
1808
|
|
|
1809
|
+
private async checkAndSyncCodex(human: HumanEntity, now: number): Promise<void> {
|
|
1810
|
+
if (this.codexImportInProgress) {
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
const codex = human.settings?.codex;
|
|
1815
|
+
const pollingInterval = codex?.polling_interval_ms ?? DEFAULT_CODEX_POLLING_MS;
|
|
1816
|
+
const lastSync = codex?.last_sync ? new Date(codex.last_sync).getTime() : 0;
|
|
1817
|
+
const timeSinceSync = now - lastSync;
|
|
1818
|
+
|
|
1819
|
+
if (timeSinceSync < pollingInterval && this.lastCodexSync > 0) {
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
this.lastCodexSync = now;
|
|
1824
|
+
const syncTimestamp = new Date().toISOString();
|
|
1825
|
+
const currentHuman = this.stateManager.getHuman();
|
|
1826
|
+
this.stateManager.setHuman({
|
|
1827
|
+
...currentHuman,
|
|
1828
|
+
settings: {
|
|
1829
|
+
...currentHuman.settings,
|
|
1830
|
+
codex: {
|
|
1831
|
+
...codex,
|
|
1832
|
+
last_sync: syncTimestamp,
|
|
1833
|
+
},
|
|
1834
|
+
},
|
|
1835
|
+
});
|
|
1836
|
+
|
|
1837
|
+
this.codexImportInProgress = true;
|
|
1838
|
+
import("../integrations/codex/importer.js")
|
|
1839
|
+
.then(({ importCodexSessions }) =>
|
|
1840
|
+
importCodexSessions({
|
|
1841
|
+
stateManager: this.stateManager,
|
|
1842
|
+
interface: this.interface,
|
|
1843
|
+
signal: this.importAbortController.signal,
|
|
1844
|
+
})
|
|
1845
|
+
)
|
|
1846
|
+
.then((result) => {
|
|
1847
|
+
if (result.sessionsProcessed > 0) {
|
|
1848
|
+
console.log(
|
|
1849
|
+
`[Processor] Codex sync complete: ${result.sessionsProcessed} sessions, ` +
|
|
1850
|
+
`${result.messagesImported} messages imported, ` +
|
|
1851
|
+
`${result.extractionScansQueued} extraction scans queued`
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
})
|
|
1855
|
+
.catch((err) => {
|
|
1856
|
+
console.warn(`[Processor] Codex sync failed:`, err);
|
|
1857
|
+
})
|
|
1858
|
+
.finally(() => {
|
|
1859
|
+
this.codexImportInProgress = false;
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1777
1863
|
private async checkAndSyncSlack(human: HumanEntity, now: number): Promise<void> {
|
|
1778
1864
|
if (this.slackImportInProgress) return;
|
|
1779
1865
|
|
|
@@ -2103,7 +2189,10 @@ const toolNextSteps = new Set([
|
|
|
2103
2189
|
}
|
|
2104
2190
|
|
|
2105
2191
|
if (typeof response.request.data.ceremony_progress === "number") {
|
|
2106
|
-
handleCeremonyProgress(this.stateManager, response.request.data.ceremony_progress);
|
|
2192
|
+
const ceremonyResult = handleCeremonyProgress(this.stateManager, response.request.data.ceremony_progress);
|
|
2193
|
+
if (ceremonyResult.wroteEiWarning) {
|
|
2194
|
+
this.interface.onMessageAdded?.("ei");
|
|
2195
|
+
}
|
|
2107
2196
|
}
|
|
2108
2197
|
|
|
2109
2198
|
if (response.request.next_step === LLMNextStep.HandleDocumentSegmentation) {
|
|
@@ -302,6 +302,7 @@ export async function buildResponsePromptData(
|
|
|
302
302
|
interested_topics: persona.topics.filter(t => t.exposure_desired - t.exposure_current > 0.2),
|
|
303
303
|
include_message_timestamps: persona.include_message_timestamps,
|
|
304
304
|
pending_update: persona.pending_update,
|
|
305
|
+
notes: persona.notes,
|
|
305
306
|
},
|
|
306
307
|
human: filteredHuman,
|
|
307
308
|
visible_personas: visiblePersonas,
|
|
@@ -395,6 +396,7 @@ export async function buildRoomResponsePromptData(
|
|
|
395
396
|
traits: respondingPersona.traits,
|
|
396
397
|
topics: respondingPersona.topics,
|
|
397
398
|
include_message_timestamps: respondingPersona.include_message_timestamps,
|
|
399
|
+
notes: respondingPersona.notes,
|
|
398
400
|
},
|
|
399
401
|
other_participants: otherParticipants,
|
|
400
402
|
human: filteredHuman,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ToolExecutor } from "../types.js";
|
|
2
|
+
import type { PersonaEntity } from "../../types.js";
|
|
3
|
+
|
|
4
|
+
export const NOTES_MAX = 20;
|
|
5
|
+
|
|
6
|
+
type GetPersona = (id: string) => PersonaEntity | null;
|
|
7
|
+
type UpdatePersona = (id: string, updates: Partial<PersonaEntity>) => boolean;
|
|
8
|
+
|
|
9
|
+
export function createAddNoteExecutor(getPersona: GetPersona, updatePersona: UpdatePersona): ToolExecutor {
|
|
10
|
+
return {
|
|
11
|
+
name: "add_note",
|
|
12
|
+
|
|
13
|
+
async execute(args: Record<string, unknown>, config?: Record<string, string>): Promise<string> {
|
|
14
|
+
const personaId = config?.persona_id ?? "";
|
|
15
|
+
const text = typeof args.text === "string" ? args.text.trim() : "";
|
|
16
|
+
console.log(`[add_note] persona="${personaId}" text="${text.slice(0, 60)}"`);
|
|
17
|
+
|
|
18
|
+
if (!personaId) {
|
|
19
|
+
return JSON.stringify({ error: "Tool misconfigured: missing persona_id" });
|
|
20
|
+
}
|
|
21
|
+
if (!text) {
|
|
22
|
+
return JSON.stringify({ error: "Missing required argument: text" });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const persona = getPersona(personaId);
|
|
26
|
+
if (!persona) {
|
|
27
|
+
return JSON.stringify({ error: "Persona not found" });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const notes = [...(persona.notes ?? [])];
|
|
31
|
+
|
|
32
|
+
if (notes.length >= NOTES_MAX) {
|
|
33
|
+
notes.shift();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
notes.push(text);
|
|
37
|
+
updatePersona(personaId, { notes });
|
|
38
|
+
|
|
39
|
+
const index = notes.length;
|
|
40
|
+
console.log(`[add_note] added note at position ${index}/${NOTES_MAX}`);
|
|
41
|
+
return JSON.stringify({ added: true, index, total: notes.length });
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createClearNoteExecutor(getPersona: GetPersona, updatePersona: UpdatePersona): ToolExecutor {
|
|
47
|
+
return {
|
|
48
|
+
name: "clear_note",
|
|
49
|
+
|
|
50
|
+
async execute(args: Record<string, unknown>, config?: Record<string, string>): Promise<string> {
|
|
51
|
+
const personaId = config?.persona_id ?? "";
|
|
52
|
+
const index = typeof args.index === "number" ? args.index : NaN;
|
|
53
|
+
console.log(`[clear_note] persona="${personaId}" index=${index}`);
|
|
54
|
+
|
|
55
|
+
if (!personaId) {
|
|
56
|
+
return JSON.stringify({ error: "Tool misconfigured: missing persona_id" });
|
|
57
|
+
}
|
|
58
|
+
if (!Number.isInteger(index) || index < 1) {
|
|
59
|
+
return JSON.stringify({ error: "index must be an integer >= 1" });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const persona = getPersona(personaId);
|
|
63
|
+
if (!persona) {
|
|
64
|
+
return JSON.stringify({ error: "Persona not found" });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const notes = [...(persona.notes ?? [])];
|
|
68
|
+
const zeroIdx = index - 1;
|
|
69
|
+
|
|
70
|
+
if (zeroIdx >= notes.length) {
|
|
71
|
+
return JSON.stringify({ error: `No note at index ${index} (total: ${notes.length})` });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
notes.splice(zeroIdx, 1);
|
|
75
|
+
updatePersona(personaId, { notes });
|
|
76
|
+
|
|
77
|
+
console.log(`[clear_note] removed note at index ${index}, remaining=${notes.length}`);
|
|
78
|
+
return JSON.stringify({ cleared: true, index, remaining: notes.length });
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
package/src/core/tools/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { tavilyWebSearchExecutor, tavilyNewsSearchExecutor } from "./builtin/web
|
|
|
12
12
|
import { currentlyPlayingExecutor } from "./builtin/currently-playing.js";
|
|
13
13
|
import { likedSongsExecutor } from "./builtin/spotify-liked-songs.js";
|
|
14
14
|
import { webFetchExecutor } from "./builtin/web-fetch.js";
|
|
15
|
+
import { NOTES_MAX } from "./builtin/persona-notes.js";
|
|
15
16
|
// file-read and list-directory are Node-only — imported lazily via registerFileReadExecutor() to avoid
|
|
16
17
|
// file-read and list-directory are Node-only — imported lazily via registerFileReadExecutor() to avoid
|
|
17
18
|
|
|
@@ -128,6 +129,61 @@ export function registerFetchMessageExecutor(executor: ToolExecutor): void {
|
|
|
128
129
|
executorRegistry.set(executor.name, executor);
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
export function registerPersonaNoteExecutors(executor1: ToolExecutor, executor2: ToolExecutor): void {
|
|
133
|
+
executorRegistry.set(executor1.name, executor1);
|
|
134
|
+
executorRegistry.set(executor2.name, executor2);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Build per-request ToolDefinition objects for the persona notes tools, injecting the
|
|
139
|
+
* current personaId via config so the shared executor knows which persona to update.
|
|
140
|
+
*/
|
|
141
|
+
export function buildPersonaNoteTools(personaId: string): ToolDefinition[] {
|
|
142
|
+
const now = new Date(0).toISOString();
|
|
143
|
+
return [
|
|
144
|
+
{
|
|
145
|
+
id: `builtin-add-note-${personaId}`,
|
|
146
|
+
provider_id: "ei",
|
|
147
|
+
name: "add_note",
|
|
148
|
+
display_name: "Add Note",
|
|
149
|
+
description: `In Ei, your system prompt can change from one turn to the next — Ei is constantly trying to provide you relevant, up-to-date information about the user and the world. If you see something in your system prompt that you don't immediately want to bring up, but want to remember, use this tool to record it for later. Additionally, if you need to remember something but cannot or should not say it directly in conversation, you can use this tool to make a note as well. Notes appear in your system prompt as a numbered list so you always see them. Limit: ${NOTES_MAX} notes (oldest evicted when full).`,
|
|
150
|
+
input_schema: {
|
|
151
|
+
type: "object",
|
|
152
|
+
properties: {
|
|
153
|
+
text: { type: "string", description: "The note to remember. Keep it concise." },
|
|
154
|
+
},
|
|
155
|
+
required: ["text"],
|
|
156
|
+
},
|
|
157
|
+
config: { persona_id: personaId },
|
|
158
|
+
runtime: "any",
|
|
159
|
+
builtin: true,
|
|
160
|
+
enabled: true,
|
|
161
|
+
created_at: now,
|
|
162
|
+
max_calls_per_interaction: 5,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: `builtin-clear-note-${personaId}`,
|
|
166
|
+
provider_id: "ei",
|
|
167
|
+
name: "clear_note",
|
|
168
|
+
display_name: "Clear Note",
|
|
169
|
+
description: "Remove a note from your scratchpad by its 1-based index (matching the numbered list in your system prompt). Use when you no longer need to track something — e.g., after you've addressed it in conversation.",
|
|
170
|
+
input_schema: {
|
|
171
|
+
type: "object",
|
|
172
|
+
properties: {
|
|
173
|
+
index: { type: "number", description: "1-based index of the note to remove" },
|
|
174
|
+
},
|
|
175
|
+
required: ["index"],
|
|
176
|
+
},
|
|
177
|
+
config: { persona_id: personaId },
|
|
178
|
+
runtime: "any",
|
|
179
|
+
builtin: true,
|
|
180
|
+
enabled: true,
|
|
181
|
+
created_at: now,
|
|
182
|
+
max_calls_per_interaction: 5,
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
|
|
131
187
|
/**
|
|
132
188
|
* Register the file_read, list_directory, directory_tree, search_files, grep, and get_file_info
|
|
133
189
|
* executors — called by Processor on TUI/Node only.
|
|
@@ -15,7 +15,7 @@ export interface DataItemBase {
|
|
|
15
15
|
learned_by?: string; // Persona ID that originally learned this item (stable UUID)
|
|
16
16
|
last_changed_by?: string; // Persona ID that most recently updated this item (stable UUID)
|
|
17
17
|
interested_personas?: string[]; // Persona IDs that have extracted/touched this item (accumulated)
|
|
18
|
-
sources?: string[]; // Namespaced source identifiers — where items were learned from. Format: "provider:id" (e.g., "opencode:ses_abc123", "cursor:composerId"). Grow-only union.
|
|
18
|
+
sources?: string[]; // Namespaced source identifiers — where items were learned from. Format: "provider:id" (e.g., "opencode:ses_abc123", "cursor:composerId", "codex:threadId"). Grow-only union.
|
|
19
19
|
persona_groups?: string[];
|
|
20
20
|
embedding?: number[];
|
|
21
21
|
rewrite_length_floor?: number; // Set after every rewrite scan: ceil(description.length * 1.1). Item is skipped by ceremony until description grows past this floor. Preserved across extraction upserts — only cleared when description exceeds it.
|
|
@@ -130,6 +130,7 @@ export interface HumanSettings {
|
|
|
130
130
|
backup?: BackupConfig;
|
|
131
131
|
claudeCode?: import("../../integrations/claude-code/types.js").ClaudeCodeSettings;
|
|
132
132
|
cursor?: import("../../integrations/cursor/types.js").CursorSettings;
|
|
133
|
+
codex?: import("../../integrations/codex/types.js").CodexSettings;
|
|
133
134
|
document?: DocumentSettings;
|
|
134
135
|
active_theme?: string;
|
|
135
136
|
custom_themes?: ThemeDefinition[];
|
|
@@ -185,6 +186,7 @@ export interface PersonaEntity {
|
|
|
185
186
|
avatar_emoji?: string; // Single emoji character used as avatar in place of initials.
|
|
186
187
|
avatar_image?: string; // Base64-encoded 64×64 image used as avatar (takes priority over avatar_emoji).
|
|
187
188
|
preferred_theme?: string; // Theme ID (built-in name or ThemeDefinition.id). Applied to chat panel when this persona is active.
|
|
189
|
+
notes?: string[]; // Private scratchpad — up to 20 short-term notes visible in the system prompt. Oldest evicted when full.
|
|
188
190
|
}
|
|
189
191
|
|
|
190
192
|
export interface PersonaCreationInput {
|
package/src/core/types/llm.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface Message {
|
|
|
25
25
|
_synthesis?: boolean; // True if message was created by multi-message synthesis
|
|
26
26
|
speaker_name?: string; // Display name of actual speaker; set on room messages for clean hydration
|
|
27
27
|
|
|
28
|
-
external?: boolean; // Set by integration importers (OpenCode, Cursor, Claude Code); invisible to LLM context
|
|
28
|
+
external?: boolean; // Set by integration importers (OpenCode, Cursor, Claude Code, Codex); invisible to LLM context
|
|
29
29
|
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* opencode:${machine}:${session}:${nativeId}
|
|
5
5
|
* claudecode:${machine}:${session}:${nativeId}
|
|
6
6
|
* cursor:${machine}:${session}:${nativeId}
|
|
7
|
+
* codex:${machine}:${session}:${nativeId}
|
|
7
8
|
* import:document:${slug}:${uuid}
|
|
8
9
|
* slack:${workspace}:${channel}:${ts}
|
|
9
10
|
*/
|
|
@@ -13,6 +14,7 @@ export type MessageIdIntegration =
|
|
|
13
14
|
| "opencode"
|
|
14
15
|
| "claudecode"
|
|
15
16
|
| "cursor"
|
|
17
|
+
| "codex"
|
|
16
18
|
| "import"
|
|
17
19
|
| "slack"
|
|
18
20
|
| "unknown"
|
|
@@ -67,6 +69,16 @@ export function parseMessageId(id: string): ParsedMessageId {
|
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
if (parts[0] === "codex" && parts.length >= 4) {
|
|
73
|
+
return {
|
|
74
|
+
integration: "codex",
|
|
75
|
+
machine: parts[1],
|
|
76
|
+
session: parts[2],
|
|
77
|
+
nativeId: parts.slice(3).join(":"),
|
|
78
|
+
raw: id,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
70
82
|
if (parts[0] === "import" && parts[1] === "document" && parts.length >= 4) {
|
|
71
83
|
return {
|
|
72
84
|
integration: "import",
|
|
@@ -109,6 +121,10 @@ export function qualifyCursorMessage(machine: string, sessionId: string, nativeI
|
|
|
109
121
|
return `cursor:${machine}:${sessionId}:${nativeId}`
|
|
110
122
|
}
|
|
111
123
|
|
|
124
|
+
export function qualifyCodexMessage(machine: string, sessionId: string, nativeId: string): string {
|
|
125
|
+
return `codex:${machine}:${sessionId}:${nativeId}`
|
|
126
|
+
}
|
|
127
|
+
|
|
112
128
|
export function qualifyDocumentMessage(slug: string, uuid: string): string {
|
|
113
129
|
return `import:document:${slug}:${uuid}`
|
|
114
130
|
}
|