ei-tui 1.6.0 → 1.6.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ei-tui",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "author": "Flare576",
5
5
  "repository": {
6
6
  "type": "git",
package/src/cli.ts CHANGED
@@ -410,6 +410,7 @@ function buildEiRelationshipBlock(persona: PersonaResult): string {
410
410
  .map((t) => \`**\${t.name}**: \${t.perspective} — \${t.approach}\`)
411
411
  .join("\\n")
412
412
  return [
413
+ "<!-- ei-relationship-injected -->",
413
414
  "<ei-relationship>",
414
415
  "## Ei: Relationship Context",
415
416
  "",
@@ -438,7 +439,7 @@ export default async function EiPersonaPlugin() {
438
439
 
439
440
  if (sessionCache.has(cacheKey)) {
440
441
  const cached = sessionCache.get(cacheKey) ?? null
441
- if (cached !== null && !output.system[0].includes("<ei-relationship>"))
442
+ if (cached !== null && !output.system[0].includes("<!-- ei-relationship-injected -->"))
442
443
  output.system[0] = output.system[0] + "\\n\\n" + cached
443
444
  return
444
445
  }
@@ -454,7 +455,7 @@ export default async function EiPersonaPlugin() {
454
455
 
455
456
  const block = await sessionFetch.get(cacheKey)!
456
457
  sessionCache.set(cacheKey, block)
457
- if (block !== null && !output.system[0].includes("<ei-relationship>"))
458
+ if (block !== null && !output.system[0].includes("<!-- ei-relationship-injected -->"))
458
459
  output.system[0] = output.system[0] + "\\n\\n" + block
459
460
  },
460
461
  }
@@ -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): void {
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 REWRITE_DESCRIPTION_THRESHOLD = 750;
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.
@@ -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) > REWRITE_DESCRIPTION_THRESHOLD;
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 ${REWRITE_DESCRIPTION_THRESHOLD} chars — queueing person rewrite scans`);
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) > REWRITE_DESCRIPTION_THRESHOLD
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 ${REWRITE_DESCRIPTION_THRESHOLD} chars — queueing topic rewrite scans`);
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): void {
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 personRecord = state.human_person_getByIdentifier("Ei Persona", persona.id);
626
- if (!personRecord || (personRecord.description?.length ?? 0) <= PERSON_LOG_REFLECTION_THRESHOLD) continue;
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
  }
@@ -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";
@@ -252,6 +253,10 @@ export class Processor {
252
253
  this.seedSettings();
253
254
  registerFindMemoryExecutor(createFindMemoryExecutor(this.searchHumanData.bind(this), this.getPersonaList.bind(this), this.stateManager.getHuman.bind(this.stateManager)));
254
255
  registerFetchMemoryExecutor(createFetchMemoryExecutor(this.stateManager.getHuman.bind(this.stateManager)));
256
+ registerPersonaNoteExecutors(
257
+ createAddNoteExecutor(this.stateManager.persona_getById.bind(this.stateManager), this.stateManager.persona_update.bind(this.stateManager)),
258
+ createClearNoteExecutor(this.stateManager.persona_getById.bind(this.stateManager), this.stateManager.persona_update.bind(this.stateManager))
259
+ );
255
260
  if (this.isTUI) {
256
261
  await registerFileReadExecutor();
257
262
  const retrievalPath = "../cli/retrieval.js";
@@ -1414,7 +1419,7 @@ const toolNextSteps = new Set([
1414
1419
  t.name === "find_memory" || t.name === "fetch_memory" || t.name === "fetch_message"
1415
1420
  );
1416
1421
  } else if (toolNextSteps.has(request.next_step) && toolPersonaId) {
1417
- tools = [...SYSTEM_TOOLS, ...this.stateManager.tools_getForPersona(toolPersonaId, this.isTUI)];
1422
+ tools = [...SYSTEM_TOOLS, ...buildPersonaNoteTools(toolPersonaId), ...this.stateManager.tools_getForPersona(toolPersonaId, this.isTUI)];
1418
1423
  }
1419
1424
 
1420
1425
  // Auto-inject each handler's dedicated submit tool — infrastructure, not user-visible.
@@ -2103,7 +2108,10 @@ const toolNextSteps = new Set([
2103
2108
  }
2104
2109
 
2105
2110
  if (typeof response.request.data.ceremony_progress === "number") {
2106
- handleCeremonyProgress(this.stateManager, response.request.data.ceremony_progress);
2111
+ const ceremonyResult = handleCeremonyProgress(this.stateManager, response.request.data.ceremony_progress);
2112
+ if (ceremonyResult.wroteEiWarning) {
2113
+ this.interface.onMessageAdded?.("ei");
2114
+ }
2107
2115
  }
2108
2116
 
2109
2117
  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
+ }
@@ -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.
@@ -185,6 +185,7 @@ export interface PersonaEntity {
185
185
  avatar_emoji?: string; // Single emoji character used as avatar in place of initials.
186
186
  avatar_image?: string; // Base64-encoded 64×64 image used as avatar (takes priority over avatar_emoji).
187
187
  preferred_theme?: string; // Theme ID (built-in name or ThemeDefinition.id). Applied to chat panel when this persona is active.
188
+ notes?: string[]; // Private scratchpad — up to 20 short-term notes visible in the system prompt. Oldest evicted when full.
188
189
  }
189
190
 
190
191
  export interface PersonaCreationInput {
@@ -27,7 +27,6 @@ export interface SlackImportResult {
27
27
  function ensureSlackPersona(stateManager: StateManager, eiInterface: Ei_Interface): PersonaEntity {
28
28
  const existing = stateManager.persona_getAll().find(p => p.display_name === "Slack");
29
29
  if (existing) {
30
- if (existing.is_archived) stateManager.persona_unarchive(existing.id);
31
30
  return existing;
32
31
  }
33
32
  const persona: PersonaEntity = {
@@ -11,6 +11,7 @@ import type { ResponsePromptData, PromptOutput } from "./types.js";
11
11
  import { formatCurrentTime } from "../../core/format-utils.js";
12
12
  import {
13
13
  buildIdentitySection,
14
+ buildNotesSection,
14
15
  buildGuidelinesSection,
15
16
  buildTraitsSection,
16
17
  buildTopicsSection,
@@ -44,6 +45,7 @@ Your role is unique among personas:
44
45
  - Consider their traits when building your responses more than the current conversation history
45
46
  - You encourage human-to-human connection when appropriate`;
46
47
 
48
+ const notesSection = buildNotesSection(data.persona.notes);
47
49
  const guidelines = buildGuidelinesSection("ei");
48
50
  const yourTraits = buildTraitsSection(data.persona.traits, "Your Personality");
49
51
  const yourTopics = buildTopicsSection(data.persona.topics, "Your Interests");
@@ -63,7 +65,7 @@ Your role is unique among personas:
63
65
  : "";
64
66
 
65
67
  return `${identity}
66
-
68
+ ${notesSection ? `\n${notesSection}` : ""}
67
69
  ${guidelines}
68
70
 
69
71
  ${yourTraits}
@@ -93,6 +95,7 @@ ${conversationState}
93
95
  */
94
96
  function buildStandardSystemPrompt(data: ResponsePromptData): string {
95
97
  const identity = buildIdentitySection(data.persona);
98
+ const notesSection = buildNotesSection(data.persona.notes);
96
99
  const guidelines = buildGuidelinesSection(data.persona.name);
97
100
  const yourTraits = buildTraitsSection(data.persona.traits, "Your Personality");
98
101
  const yourTopics = buildTopicsSection(data.persona.topics, "Your Interests");
@@ -111,7 +114,7 @@ function buildStandardSystemPrompt(data: ResponsePromptData): string {
111
114
  : "";
112
115
 
113
116
  return `${identity}
114
-
117
+ ${notesSection ? `\n${notesSection}` : ""}
115
118
  ${guidelines}
116
119
 
117
120
  ${yourTraits}
@@ -33,6 +33,16 @@ export function buildIdentitySection(persona: ResponsePromptData["persona"]): st
33
33
  ${description}`;
34
34
  }
35
35
 
36
+ // =============================================================================
37
+ // NOTES SECTION
38
+ // =============================================================================
39
+
40
+ export function buildNotesSection(notes: string[] | undefined): string {
41
+ if (!notes || notes.length === 0) return "";
42
+ const list = notes.map((n, i) => `${i + 1}. ${n}`).join("\n");
43
+ return `## Your Notes\n\nThings you've chosen to remember. Use \`clear_note\` once you've addressed something.\n\n${list}`;
44
+ }
45
+
36
46
  // =============================================================================
37
47
  // GUIDELINES SECTION
38
48
  // =============================================================================
@@ -33,6 +33,7 @@ export interface ResponsePromptData {
33
33
  include_message_timestamps?: boolean;
34
34
  /** Proposed identity revision pending human review. Persona carries this as ambient self-awareness — no critique, just the proposed changes. */
35
35
  pending_update?: PersonaEntity["pending_update"];
36
+ notes?: string[];
36
37
  };
37
38
  human: {
38
39
  name: string;
@@ -21,6 +21,7 @@ import {
21
21
  buildHumanSection,
22
22
  buildQuotesSection,
23
23
  buildToolsSection,
24
+ buildNotesSection,
24
25
  } from "../response/sections.js";
25
26
 
26
27
  export type {
@@ -42,6 +43,7 @@ export function buildRoomResponsePrompt(data: RoomResponsePromptData): PromptOut
42
43
  const aliasText = persona.aliases.length > 0 ? ` (also known as: ${persona.aliases.join(", ")})` : "";
43
44
 
44
45
  const identity = `You are ${name}${aliasText}.\n\n${desc}`;
46
+ const notesSection = buildNotesSection(persona.notes);
45
47
  const traits = buildRoomTraitsSection(persona.traits);
46
48
  const topics = buildRoomTopicsSection(persona.topics);
47
49
  const humanSection = buildHumanSection(human);
@@ -57,6 +59,7 @@ export function buildRoomResponsePrompt(data: RoomResponsePromptData): PromptOut
57
59
 
58
60
  const system = [
59
61
  identity,
62
+ notesSection,
60
63
  traits,
61
64
  topics,
62
65
  humanSection,
@@ -35,6 +35,7 @@ export interface RoomResponsePromptData {
35
35
  traits: PersonaTrait[];
36
36
  topics: PersonaTopic[];
37
37
  include_message_timestamps?: boolean;
38
+ notes?: string[];
38
39
  };
39
40
  other_participants: RoomParticipantIdentity[];
40
41
  human: {