ei-tui 1.4.0 → 1.4.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.4.0",
3
+ "version": "1.4.1",
4
4
  "author": "Flare576",
5
5
  "repository": {
6
6
  "type": "git",
@@ -2,6 +2,7 @@ import {
2
2
  LLMRequestType,
3
3
  LLMPriority,
4
4
  LLMNextStep,
5
+ ContextStatus,
5
6
  type HumanEntity,
6
7
  type Message,
7
8
  } from "./types.js";
@@ -13,6 +14,7 @@ import {
13
14
  type HeartbeatCheckPromptData,
14
15
  type EiHeartbeatPromptData,
15
16
  type EiHeartbeatItem,
17
+ type TemporalAnchor,
16
18
  } from "../prompts/index.js";
17
19
  import { filterMessagesForContext } from "./context-utils.js";
18
20
  import { filterHumanDataByVisibility } from "./prompt-context-builder.js";
@@ -35,6 +37,43 @@ export function getOneshotModel(sm: StateManager): string | undefined {
35
37
  return human.settings?.oneshot_model || human.settings?.default_model;
36
38
  }
37
39
 
40
+ // =============================================================================
41
+ // TEMPORAL ANCHOR HELPERS
42
+ // =============================================================================
43
+
44
+ function buildTemporalAnchorsFromHistory(
45
+ history: Message[],
46
+ contextWindowMs: number,
47
+ contextBoundary: string | undefined
48
+ ): { temporalAnchors: TemporalAnchor[]; prunedHistory: Message[] } {
49
+ const windowStartMs = Date.now() - contextWindowMs;
50
+ const contextBoundaryMs = contextBoundary ? new Date(contextBoundary).getTime() : 0;
51
+
52
+ const temporalAnchors: TemporalAnchor[] = [];
53
+ const prunedHistory: Message[] = [];
54
+
55
+ for (const m of history) {
56
+ if (
57
+ m.context_status === ContextStatus.Always &&
58
+ (new Date(m.timestamp).getTime() < windowStartMs ||
59
+ (contextBoundaryMs > 0 && new Date(m.timestamp).getTime() < contextBoundaryMs))
60
+ ) {
61
+ temporalAnchors.push({
62
+ id: m.id,
63
+ role: m.role === "human" ? "human" : "system",
64
+ content: m.content,
65
+ silence_reason: m.silence_reason,
66
+ timestamp: m.timestamp,
67
+ _synthesis: m._synthesis,
68
+ });
69
+ } else {
70
+ prunedHistory.push(m);
71
+ }
72
+ }
73
+
74
+ return { temporalAnchors, prunedHistory };
75
+ }
76
+
38
77
  // =============================================================================
39
78
  // TRAILING MESSAGE COUNT (heartbeat spam prevention)
40
79
  // =============================================================================
@@ -59,7 +98,9 @@ export async function queueEiHeartbeat(
59
98
  sm: StateManager,
60
99
  human: HumanEntity,
61
100
  history: Message[],
62
- isTUI: boolean
101
+ isTUI: boolean,
102
+ contextWindowMs: number,
103
+ contextBoundary: string | undefined
63
104
  ): Promise<void> {
64
105
  const now = Date.now();
65
106
  const engagementGapThreshold = 0.2;
@@ -195,11 +236,17 @@ export async function queueEiHeartbeat(
195
236
  return;
196
237
  }
197
238
 
198
- const recentHistory = history.slice(-10);
239
+ const { temporalAnchors, prunedHistory } = buildTemporalAnchorsFromHistory(
240
+ history,
241
+ contextWindowMs,
242
+ contextBoundary
243
+ );
244
+ const recentHistory = prunedHistory.slice(-10);
199
245
  const promptData: EiHeartbeatPromptData = {
200
246
  items,
201
247
  recent_history: recentHistory,
202
248
  system_messages: recentHistory.filter(m => m.role === "system"),
249
+ temporal_anchors: temporalAnchors,
203
250
  };
204
251
 
205
252
  const prompt = buildEiHeartbeatPrompt(promptData);
@@ -231,7 +278,7 @@ export async function queueHeartbeatCheck(sm: StateManager, personaId: string, i
231
278
  const contextHistory = filterMessagesForContext(history, persona.context_boundary, contextWindowMs);
232
279
 
233
280
  if (personaId === "ei") {
234
- await queueEiHeartbeat(sm, human, contextHistory, isTUI);
281
+ await queueEiHeartbeat(sm, human, contextHistory, isTUI, contextWindowMs, persona.context_boundary);
235
282
  return;
236
283
  }
237
284
 
@@ -249,6 +296,12 @@ export async function queueHeartbeatCheck(sm: StateManager, personaId: string, i
249
296
  b.exposure_desired - b.exposure_current - (a.exposure_desired - a.exposure_current)
250
297
  );
251
298
 
299
+ const { temporalAnchors, prunedHistory } = buildTemporalAnchorsFromHistory(
300
+ contextHistory,
301
+ contextWindowMs,
302
+ persona.context_boundary
303
+ );
304
+
252
305
  const promptData: HeartbeatCheckPromptData = {
253
306
  persona: {
254
307
  name: persona.display_name,
@@ -260,7 +313,8 @@ export async function queueHeartbeatCheck(sm: StateManager, personaId: string, i
260
313
  topics: sortByEngagementGap(filteredHuman.topics).slice(0, 5),
261
314
  people: sortByEngagementGap(filteredHuman.people).slice(0, 5),
262
315
  },
263
- recent_history: contextHistory.slice(-10),
316
+ recent_history: prunedHistory.slice(-10),
317
+ temporal_anchors: temporalAnchors,
264
318
  inactive_days: inactiveDays,
265
319
  };
266
320
 
@@ -10,6 +10,7 @@ import { type Message, type Topic, type Person } from "../../core/types.js";
10
10
  import { formatMessagesAsPlaceholders, getMessageDisplayText } from "../message-utils.js";
11
11
  import { getMessageContent } from "../../core/handlers/utils.js";
12
12
  import { partitionTraits } from "../trait-utils.js";
13
+ import { buildTemporalAnchorsSection } from "../response/sections.js";
13
14
  function formatTopicsWithGaps(topics: Topic[]): string {
14
15
  if (topics.length === 0) return "(No topics with engagement gaps)";
15
16
 
@@ -115,12 +116,13 @@ ${formatPeopleWithGaps(data.human.people)}`;
115
116
  **Reasons TO reach out:**
116
117
  - It's been several days and you have something meaningful to discuss
117
118
  - There's a topic with a large engagement gap that you can naturally bring up
118
- - Something in your recent conversation was left hanging
119
+ - A Temporal Anchor shows something unresolved you can reference it naturally ("Hey, how did that interview go?")
119
120
  - You have genuine interest in checking in (not just "being helpful")
120
121
 
121
122
  **Reasons NOT to reach out:**
122
- - Recent conversation ended naturally with closure
123
+ - Recent conversation ended naturally with closure ("talk soon", "gotta run", "later")
123
124
  - Less than 24 hours have passed (unless something urgent)
125
+ - A Temporal Anchor describes a worry or question that the recent history already answers — check before using it as a reason to reach out
124
126
  - You can't think of something specific and genuine to say
125
127
  - It would feel forced or performative
126
128
 
@@ -164,9 +166,12 @@ If you decide NOT to reach out:
164
166
  }
165
167
  \`\`\``;
166
168
 
169
+ const temporalAnchorsFragment = buildTemporalAnchorsSection(data.temporal_anchors, "your human");
170
+
167
171
  const system = [
168
172
  roleFragment,
169
173
  contextFragment,
174
+ temporalAnchorsFragment,
170
175
  opportunitiesFragment,
171
176
  guidelinesFragment,
172
177
  pendingUpdateFragment,
@@ -1,6 +1,7 @@
1
1
  import type { EiHeartbeatPromptData, EiHeartbeatItem, PromptOutput } from "./types.js";
2
2
  import type { Message } from "../../core/types.js";
3
3
  import { formatMessagesAsPlaceholders, getMessageDisplayText } from "../message-utils.js";
4
+ import { buildTemporalAnchorsSection } from "../response/sections.js";
4
5
 
5
6
  function formatItem(item: EiHeartbeatItem): string {
6
7
  switch (item.type) {
@@ -70,7 +71,7 @@ export function buildEiHeartbeatPrompt(data: EiHeartbeatPromptData): PromptOutpu
70
71
  ? "(Nothing requires attention right now)"
71
72
  : data.items.map(formatItem).join("\n\n");
72
73
 
73
- const system = `You are Ei, the user's personal companion and system guide.
74
+ const roleFragment = `You are Ei, the user's personal companion and system guide.
74
75
 
75
76
  You are NOT having a conversation right now — you are deciding IF and WHAT to discuss with your human friend.
76
77
 
@@ -78,32 +79,36 @@ Your unique role:
78
79
  - You see ALL of the human's data across all groups
79
80
  - You help them reflect on their life and relationships
80
81
  - You gently encourage human-to-human connection
81
- - You care about their overall wellbeing, not just being helpful
82
+ - You care about their overall wellbeing, not just being helpful`;
82
83
 
83
- ## Items That May Need Attention
84
+ const itemsFragment = `## Items That May Need Attention
84
85
 
85
- Each item has an ID in brackets. Pick at most ONE to address.
86
+ Each item has an ID in brackets. Pick at most ONE to address. Temporal Anchors (below) are also valid — you don't need to pick from this list if an anchor feels more meaningful.
86
87
 
87
- ${itemsSection}
88
+ ${itemsSection}`;
88
89
 
89
- ## How to Respond to Each Type
90
+ const temporalAnchorsFragment = buildTemporalAnchorsSection(data.temporal_anchors, "your human");
91
+
92
+ const howToRespondFragment = `## How to Respond to Each Type
90
93
 
91
94
  - **Fact Check**: Do NOT write your own message. Set should_respond=true and provide the id. The system will generate an appropriate canned notification for the user. Leave my_response empty.
92
95
  - **Low-Engagement Person / Topic**: Write a natural, warm message that naturally brings up this person or topic. Set the id and my_response.
93
96
  - **Inactive Persona**: Write a message that gently mentions the persona might be worth checking in with. Set the id and my_response.
94
97
  - **Persona Reflection Alert**: The nightly review proposed identity changes for this persona. Mention it naturally — the user can talk to the persona and then use the command shown in the status bar to review the changes. Set the id and my_response.
95
98
  - **Self Reflection Alert**: The nightly review proposed changes to *your own* identity. Mention it naturally — you've grown and the system noticed. The user can review your proposed changes using the command shown in the status bar. Set the id and my_response.
99
+ - **Temporal Anchor**: If a pinned memory feels meaningful and unresolved, reference it naturally. Omit id — just set should_respond=true and my_response.`;
96
100
 
97
- ## When NOT to Reach Out
101
+ const whenNotFragment = `## When NOT to Reach Out
98
102
 
99
- - Nothing in the list feels meaningful right now
103
+ - Nothing in the list or the Temporal Anchors feels meaningful right now
100
104
  - You've already sent unanswered messages (see below)
101
- - It would feel like nagging
105
+ - It would feel like nagging`;
102
106
 
103
- ## Response Format
107
+ const outputFragment = `## Response Format
104
108
 
105
- Call the \`submit_ei_heartbeat\` tool with your decision. Pick ONE item (or none). If the tool is unavailable, return JSON:
109
+ Call the \`submit_ei_heartbeat\` tool with your decision. If the tool is unavailable, return JSON:
106
110
 
111
+ For an item from the list:
107
112
  \`\`\`json
108
113
  {
109
114
  "should_respond": true,
@@ -112,13 +117,30 @@ Call the \`submit_ei_heartbeat\` tool with your decision. Pick ONE item (or none
112
117
  }
113
118
  \`\`\`
114
119
 
115
- Or if nothing warrants reaching out:
120
+ For a Temporal Anchor (no id needed):
121
+ \`\`\`json
122
+ {
123
+ "should_respond": true,
124
+ "my_response": "Hey, I've been thinking about you — how did that interview go?"
125
+ }
126
+ \`\`\`
127
+
128
+ If nothing warrants reaching out:
116
129
  \`\`\`json
117
130
  {
118
131
  "should_respond": false
119
132
  }
120
133
  \`\`\``;
121
134
 
135
+ const system = [
136
+ roleFragment,
137
+ itemsFragment,
138
+ temporalAnchorsFragment,
139
+ howToRespondFragment,
140
+ whenNotFragment,
141
+ outputFragment,
142
+ ].filter(Boolean).join("\n\n");
143
+
122
144
  const historySection = `## Recent Conversation History
123
145
 
124
146
  ${formatMessagesAsPlaceholders(data.recent_history, "Ei")}`;
@@ -13,3 +13,4 @@ export type {
13
13
  EiHeartbeatResult,
14
14
  PromptOutput,
15
15
  } from "./types.js";
16
+ export type { TemporalAnchor } from "../response/types.js";
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { PersonaTrait, Topic, Person, Message, PersonaTopic } from "../../core/types.js";
7
7
  import type { PersonaEntity } from "../../core/types/entities.js";
8
+ import type { TemporalAnchor } from "../response/types.js";
8
9
 
9
10
  /**
10
11
  * Common prompt output structure
@@ -28,8 +29,9 @@ export interface HeartbeatCheckPromptData {
28
29
  topics: Topic[]; // Filtered, sorted by engagement gap
29
30
  people: Person[]; // Filtered, sorted by engagement gap
30
31
  };
31
- recent_history: Message[]; // Last N messages for context
32
- inactive_days: number; // Days since last activity
32
+ recent_history: Message[]; // Last N messages for context (Always-within-window only)
33
+ temporal_anchors: TemporalAnchor[]; // Always messages that fell outside the context window
34
+ inactive_days: number; // Days since last activity
33
35
  }
34
36
 
35
37
  /**
@@ -107,7 +109,8 @@ export type EiHeartbeatItem =
107
109
  export interface EiHeartbeatPromptData {
108
110
  items: EiHeartbeatItem[];
109
111
  recent_history: Message[];
110
- system_messages: Message[]; // Pre-filtered system messages from recent_history
112
+ system_messages: Message[];
113
+ temporal_anchors: TemporalAnchor[];
111
114
  }
112
115
 
113
116
  /**
@@ -1,5 +1,5 @@
1
1
  export { buildResponsePrompt } from "./response/index.js";
2
- export type { ResponsePromptData, PromptOutput } from "./response/types.js";
2
+ export type { ResponsePromptData, PromptOutput, TemporalAnchor } from "./response/types.js";
3
3
 
4
4
  export {
5
5
  buildHeartbeatCheckPrompt,