ei-tui 0.9.0 → 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ei-tui",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "author": "Flare576",
5
5
  "repository": {
6
6
  "type": "git",
@@ -135,9 +135,9 @@ export function handleReflectionCritic(response: LLMResponse, state: StateManage
135
135
  if (personRecord) {
136
136
  state.human_person_upsert({
137
137
  ...personRecord,
138
- description: result.updated_identity.long_description,
138
+ description: "",
139
139
  });
140
- console.log(`[ReflectionCritic ${personaDisplayName}] Person record description replaced (was log, now distilled identity)`);
140
+ console.log(`[ReflectionCritic ${personaDisplayName}] Person record description cleared ready for fresh evidence after reflection`);
141
141
  }
142
142
 
143
143
  const persona = state.persona_getById(personaId);
@@ -298,6 +298,15 @@ export async function handleHumanPersonScan(response: LLMResponse, state: StateM
298
298
  }
299
299
  }
300
300
 
301
+ if (matchedPerson && response.request.data.reflection_progress === 1) {
302
+ const linkedPersonaId = matchedPerson.identifiers
303
+ ?.find(i => i.type === "Ei Persona")?.value;
304
+ if (linkedPersonaId) {
305
+ console.log(`[handleHumanPersonScan] Skipping update for "${candidate.name}" — scan marked as reflection drain (reflection_progress=1)`);
306
+ continue;
307
+ }
308
+ }
309
+
301
310
  const matchResult: ItemMatchResult = { matched_guid: matchedPerson?.id ?? null };
302
311
  queuePersonUpdate(matchResult, {
303
312
  ...context,
@@ -253,6 +253,8 @@ export function checkAndQueueHumanExtraction(
253
253
  const unextractedPeople = sm.messages_getUnextracted(personaId, "p", undefined, "exclude");
254
254
  const peopleThreshold = Math.min(EXTRACTION_TAPER_CAP, human.people.length);
255
255
  if (unextractedPeople.length > 0 && unextractedPeople.length >= peopleThreshold) {
256
+ const personaForScan = sm.persona_getById(personaId);
257
+ const personScanOptions = personaForScan?.pending_update ? { reflection_progress: 1 } : undefined;
256
258
  const context: ExtractionContext = {
257
259
  personaId,
258
260
  personaDisplayName,
@@ -260,9 +262,9 @@ export function checkAndQueueHumanExtraction(
260
262
  messages_analyze: unextractedPeople,
261
263
  extraction_flag: "p",
262
264
  };
263
- queuePersonScan(context, sm);
265
+ queuePersonScan(context, sm, personScanOptions);
264
266
  console.log(
265
- `[Processor] Human Seed extraction: people (threshold: ${peopleThreshold}, unextracted: ${unextractedPeople.length})`
267
+ `[Processor] Human Seed extraction: people (threshold: ${peopleThreshold}, unextracted: ${unextractedPeople.length}${personScanOptions ? ", reflection_progress=1" : ""})`
266
268
  );
267
269
  }
268
270
  }
@@ -139,7 +139,10 @@ function queueExposurePhase(personaId: string, state: StateManager, options?: Ex
139
139
  messages_analyze: unextractedPeople,
140
140
  extraction_flag: "p",
141
141
  };
142
- queuePersonScan(context, state, options);
142
+ const personScanOptions = persona.pending_update
143
+ ? { ...options, reflection_progress: 1 }
144
+ : options;
145
+ queuePersonScan(context, state, personScanOptions);
143
146
  }
144
147
 
145
148
  const totalUnextracted = unextractedFacts.length + unextractedTopics.length + unextractedPeople.length;
@@ -445,6 +448,35 @@ const REWRITE_DESCRIPTION_THRESHOLD = 750;
445
448
  * Phase 2 items enqueue at Normal priority, naturally processing before more
446
449
  * Low-priority Phase 1 scans.
447
450
  */
451
+ /**
452
+ * Forces an unconditional, threshold-bypassing Person scan on Apply/Dismiss.
453
+ * Cannot be replaced by checkAndQueueHumanExtraction — that function gates on
454
+ * MIN(10, people_count) and would silently skip messages if the threshold isn't
455
+ * met, leaving reflection-era noise unprocessed and ungated.
456
+ */
457
+ export function queueReflectionDrain(personaId: string, state: StateManager): void {
458
+ const persona = state.persona_getById(personaId);
459
+ if (!persona) return;
460
+
461
+ const allMessages = state.messages_get(personaId);
462
+ const unextractedPeople = state.messages_getUnextracted(personaId, "p");
463
+
464
+ if (unextractedPeople.length === 0) {
465
+ console.log(`[reflection:drain] No unextracted messages for ${persona.display_name} — drain complete`);
466
+ return;
467
+ }
468
+
469
+ const context: ExtractionContext = {
470
+ personaId,
471
+ personaDisplayName: persona.display_name,
472
+ messages_context: allMessages.filter(m => m.p === true),
473
+ messages_analyze: unextractedPeople,
474
+ extraction_flag: "p",
475
+ };
476
+ queuePersonScan(context, state, { reflection_progress: 1 });
477
+ console.log(`[reflection:drain] Queued Person scan for ${persona.display_name} (${unextractedPeople.length} messages) — clears on completion`);
478
+ }
479
+
448
480
  export function queueRewritePhase(state: StateManager): void {
449
481
  const human = state.getHuman();
450
482
  const rewriteModel = human.settings?.rewrite_model;
@@ -67,6 +67,8 @@ export interface ExtractionContext {
67
67
  export interface ExtractionOptions {
68
68
  /** Ceremony phase number (1=Dedup, 2=Expose) */
69
69
  ceremony_progress?: number;
70
+ /** Set to 1 on scans queued while there is a Pending Reflection. */
71
+ reflection_progress?: number;
70
72
  /** Override model for extraction LLM calls */
71
73
  extraction_model?: string;
72
74
  /**
@@ -19,6 +19,7 @@ export {
19
19
  shouldStartCeremony,
20
20
  startCeremony,
21
21
  handleCeremonyProgress,
22
+ queueReflectionDrain,
22
23
  prunePersonaMessages,
23
24
  runHumanCeremony,
24
25
  } from "./ceremony.js";
@@ -39,7 +39,7 @@ 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 { shouldStartCeremony, startCeremony, handleCeremonyProgress, queueUserDedupRequest, queueRoomCapture, queuePersonaCapture, checkAndQueueRoomExtraction, queueTargetedPersonUpdate, queueTargetedTopicUpdate } from "./orchestrators/index.js";
42
+ import { shouldStartCeremony, startCeremony, handleCeremonyProgress, queueReflectionDrain, queueUserDedupRequest, queueRoomCapture, queuePersonaCapture, checkAndQueueRoomExtraction, queueTargetedPersonUpdate, queueTargetedTopicUpdate } from "./orchestrators/index.js";
43
43
  import { BUILT_IN_FACTS } from "./constants/built-in-facts.js";
44
44
  import { DEFAULT_SEED_TRAITS } from "./constants/seed-traits.js";
45
45
 
@@ -1755,6 +1755,43 @@ const toolNextSteps = new Set([
1755
1755
  if (ok) this.interface.onPersonaUpdated?.(personaId);
1756
1756
  }
1757
1757
 
1758
+ async finalizeReflection(
1759
+ personaId: string,
1760
+ action: "apply" | "dismiss",
1761
+ identity?: { short_description?: string; long_description: string; traits: NonNullable<PersonaEntity["pending_update"]>["traits"]; topics: NonNullable<PersonaEntity["pending_update"]>["topics"] }
1762
+ ): Promise<void> {
1763
+ const persona = this.stateManager.persona_getById(personaId);
1764
+ if (!persona) return;
1765
+
1766
+ const source = identity ?? (persona.pending_update ? {
1767
+ short_description: persona.pending_update.short_description,
1768
+ long_description: persona.pending_update.long_description,
1769
+ traits: persona.pending_update.traits,
1770
+ topics: persona.pending_update.topics,
1771
+ } : null);
1772
+
1773
+ const updates: Partial<PersonaEntity> = { pending_update: undefined };
1774
+
1775
+ if (action === "apply" && source) {
1776
+ updates.short_description = source.short_description;
1777
+ updates.long_description = source.long_description;
1778
+ updates.traits = source.traits.map(t => ({
1779
+ ...t,
1780
+ id: t.id?.startsWith("pending-") ? crypto.randomUUID() : t.id,
1781
+ }));
1782
+ updates.topics = source.topics.map(t => ({
1783
+ ...t,
1784
+ id: t.id?.startsWith("pending-") ? crypto.randomUUID() : t.id,
1785
+ }));
1786
+ }
1787
+
1788
+ const ok = await updatePersona(this.stateManager, personaId, updates);
1789
+ if (ok) {
1790
+ queueReflectionDrain(personaId, this.stateManager);
1791
+ this.interface.onPersonaUpdated?.(personaId);
1792
+ }
1793
+ }
1794
+
1758
1795
  async updateRoom(roomId: string, updates: Partial<RoomEntity>): Promise<void> {
1759
1796
  const ok = this.stateManager.updateRoom(roomId, updates);
1760
1797
  if (ok) this.interface.onRoomUpdated?.(roomId);
@@ -233,7 +233,7 @@ export const personaCommand: Command = {
233
233
  }
234
234
 
235
235
  if (reviewResult.content === null) {
236
- await ctx.ei.updatePersona(personaId, { pending_update: undefined });
236
+ await ctx.ei.finalizeReflection(personaId, "dismiss");
237
237
  ctx.showNotification(`Dismissed pending changes for ${persona.display_name}`, "info");
238
238
  return;
239
239
  }
@@ -267,17 +267,16 @@ export const personaCommand: Command = {
267
267
  return;
268
268
  }
269
269
 
270
- await ctx.ei.updatePersona(personaId, { pending_update: undefined });
270
+ await ctx.ei.finalizeReflection(personaId, "dismiss");
271
271
  ctx.showNotification(`Dismissed pending changes for ${persona.display_name}`, "info");
272
272
  return;
273
273
  }
274
274
 
275
- await ctx.ei.updatePersona(personaId, {
275
+ await ctx.ei.finalizeReflection(personaId, "apply", {
276
276
  long_description: previewParsed.long_description,
277
277
  short_description: previewParsed.short_description,
278
278
  traits: previewParsed.traits,
279
279
  topics: previewParsed.topics,
280
- pending_update: undefined,
281
280
  });
282
281
  ctx.showNotification(`Applied changes to ${persona.display_name}`, "info");
283
282
  return;
@@ -327,13 +327,7 @@ export const reflectCommand: Command = {
327
327
  topics: persona.pending_update!.topics,
328
328
  };
329
329
 
330
- await ctx.ei.updatePersona(personaId, {
331
- long_description: source.long_description,
332
- short_description: source.short_description,
333
- traits: source.traits,
334
- topics: source.topics,
335
- pending_update: undefined,
336
- });
330
+ await ctx.ei.finalizeReflection(personaId, "apply", source);
337
331
 
338
332
  if (fs.existsSync(folderPath)) {
339
333
  fs.rmSync(folderPath, { recursive: true, force: true });
@@ -363,7 +357,7 @@ export const reflectCommand: Command = {
363
357
  return;
364
358
  }
365
359
 
366
- await ctx.ei.updatePersona(personaId, { pending_update: undefined });
360
+ await ctx.ei.finalizeReflection(personaId, "dismiss");
367
361
  const folderPath = getReflectFolder(persona);
368
362
  if (fs.existsSync(folderPath)) {
369
363
  fs.rmSync(folderPath, { recursive: true, force: true });
@@ -113,6 +113,7 @@ export function MessageList() {
113
113
  stickyScroll={true}
114
114
  stickyStart="bottom"
115
115
  viewportCulling={true}
116
+ wrapperOptions={{ paddingRight: 2 }}
116
117
  >
117
118
  <For each={messagesWithQuotes()}>
118
119
  {(message, index) => {
@@ -159,7 +160,7 @@ export function MessageList() {
159
160
  attributes={TextAttributes.BOLD}
160
161
  content={header()}
161
162
  />
162
- <box marginLeft={2}>
163
+ <box paddingLeft={2} flexGrow={1}>
163
164
  <markdown
164
165
  content={displayContent}
165
166
  syntaxStyle={solarizedDarkSyntax}
@@ -143,6 +143,7 @@ export function RoomMessageList() {
143
143
  stickyScroll={true}
144
144
  stickyStart="bottom"
145
145
  viewportCulling={true}
146
+ wrapperOptions={{ paddingRight: 2 }}
146
147
  >
147
148
  <For each={displayMessagesWithQuotes()}>
148
149
  {(msg) => {
@@ -172,10 +173,10 @@ export function RoomMessageList() {
172
173
  attributes={TextAttributes.BOLD}
173
174
  content={header}
174
175
  />
175
- <box marginLeft={2} visible={isSilence}>
176
+ <box paddingLeft={2} flexGrow={1} visible={isSilence}>
176
177
  <text fg="#586e75" content={silenceText} />
177
178
  </box>
178
- <box marginLeft={2} visible={!isSilence}>
179
+ <box paddingLeft={2} flexGrow={1} visible={!isSilence}>
179
180
  <markdown
180
181
  content={normalContent}
181
182
  syntaxStyle={solarizedDarkSyntax}
@@ -80,6 +80,7 @@ export interface EiContextValue {
80
80
  deletePersona: (personaId: string) => Promise<void>;
81
81
  setContextBoundary: (personaId: string, timestamp: string | null) => Promise<void>;
82
82
  updatePersona: (personaId: string, updates: Partial<PersonaEntity>) => Promise<void>;
83
+ finalizeReflection: (personaId: string, action: "apply" | "dismiss", identity?: { short_description?: string; long_description: string; traits: NonNullable<PersonaEntity["pending_update"]>["traits"]; topics: NonNullable<PersonaEntity["pending_update"]>["topics"] }) => Promise<void>;
83
84
  getPersona: (personaId: string) => Promise<PersonaEntity | null>;
84
85
  resolvePersonaName: (nameOrAlias: string) => Promise<string | null>;
85
86
  getHuman: () => Promise<HumanEntity>;
@@ -360,6 +361,16 @@ export const EiProvider: ParentComponent = (props) => {
360
361
  await refreshPersonas();
361
362
  };
362
363
 
364
+ const finalizeReflection = async (
365
+ personaId: string,
366
+ action: "apply" | "dismiss",
367
+ identity?: { short_description?: string; long_description: string; traits: NonNullable<PersonaEntity["pending_update"]>["traits"]; topics: NonNullable<PersonaEntity["pending_update"]>["topics"] }
368
+ ) => {
369
+ if (!processor) return;
370
+ await processor.finalizeReflection(personaId, action, identity);
371
+ await refreshPersonas();
372
+ };
373
+
363
374
  const getPersona = async (personaId: string) => {
364
375
  if (!processor) return null;
365
376
  return processor.getPersona(personaId);
@@ -937,6 +948,7 @@ export const EiProvider: ParentComponent = (props) => {
937
948
  deletePersona,
938
949
  setContextBoundary,
939
950
  updatePersona,
951
+ finalizeReflection,
940
952
  getPersona,
941
953
  resolvePersonaName,
942
954
  getHuman,