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.
Files changed (44) hide show
  1. package/README.md +36 -35
  2. package/package.json +6 -2
  3. package/src/README.md +85 -1
  4. package/src/cli/README.md +30 -20
  5. package/src/cli/retrieval.ts +5 -17
  6. package/src/cli.ts +69 -0
  7. package/src/core/handlers/index.ts +195 -172
  8. package/src/core/orchestrators/ceremony.ts +4 -4
  9. package/src/core/orchestrators/extraction-chunker.ts +3 -3
  10. package/src/core/processor.ts +245 -77
  11. package/src/core/queue-processor.ts +3 -26
  12. package/src/core/state/checkpoints.ts +4 -0
  13. package/src/core/state/personas.ts +13 -1
  14. package/src/core/state/queue.ts +80 -23
  15. package/src/core/state-manager.ts +36 -10
  16. package/src/core/types.ts +23 -11
  17. package/src/core/utils/crossFind.ts +44 -0
  18. package/src/core/utils/index.ts +4 -0
  19. package/src/integrations/opencode/importer.ts +118 -691
  20. package/src/prompts/heartbeat/check.ts +27 -13
  21. package/src/prompts/heartbeat/ei.ts +65 -136
  22. package/src/prompts/heartbeat/types.ts +47 -17
  23. package/src/prompts/human/item-update.ts +20 -8
  24. package/src/prompts/index.ts +2 -5
  25. package/src/prompts/message-utils.ts +42 -3
  26. package/src/prompts/response/index.ts +13 -6
  27. package/src/prompts/response/sections.ts +65 -12
  28. package/src/prompts/response/types.ts +10 -0
  29. package/tui/README.md +89 -4
  30. package/tui/src/commands/dlq.ts +75 -0
  31. package/tui/src/commands/editor.tsx +1 -1
  32. package/tui/src/commands/queue.ts +77 -0
  33. package/tui/src/components/CommandSuggest.tsx +50 -0
  34. package/tui/src/components/MessageList.tsx +12 -2
  35. package/tui/src/components/PromptInput.tsx +118 -30
  36. package/tui/src/components/Sidebar.tsx +6 -2
  37. package/tui/src/components/StatusBar.tsx +12 -5
  38. package/tui/src/context/ei.tsx +43 -3
  39. package/tui/src/context/keyboard.tsx +90 -2
  40. package/tui/src/util/clipboard.ts +73 -0
  41. package/tui/src/util/yaml-serializers.ts +81 -11
  42. package/src/prompts/validation/ei.ts +0 -93
  43. package/src/prompts/validation/index.ts +0 -6
  44. package/src/prompts/validation/types.ts +0 -22
@@ -1,4 +1,5 @@
1
- import type { LLMRequest, LLMNextStep, QueueFailResult } from "../types.js";
1
+ import type { LLMRequest, QueueFailResult } from "../types.js";
2
+ import { DLQ_MAX_COUNT, DLQ_MAX_AGE_DAYS } from "../types.js";
2
3
 
3
4
  const BASE_BACKOFF_MS = 2_000;
4
5
  const MAX_BACKOFF_MS = 30_000;
@@ -27,35 +28,55 @@ export class QueueState {
27
28
  private paused = false;
28
29
 
29
30
  load(queue: LLMRequest[]): void {
30
- this.queue = queue;
31
+ // Reset any items stuck in 'processing' from a previous session
32
+ this.queue = queue.map(r =>
33
+ r.state === "processing" ? { ...r, state: "pending" } : r
34
+ );
31
35
  }
32
36
 
33
37
  export(): LLMRequest[] {
34
38
  return this.queue;
35
39
  }
36
40
 
37
- enqueue(request: Omit<LLMRequest, "id" | "created_at" | "attempts">): string {
41
+ enqueue(request: Omit<LLMRequest, "id" | "created_at" | "attempts" | "state">): string {
38
42
  const id = crypto.randomUUID();
39
43
  const fullRequest: LLMRequest = {
40
44
  ...request,
41
45
  id,
42
46
  created_at: new Date().toISOString(),
43
47
  attempts: 0,
48
+ state: "pending",
44
49
  };
45
50
  this.queue.push(fullRequest);
46
51
  return id;
47
52
  }
48
53
 
49
- peekHighest(): LLMRequest | null {
54
+ claimHighest(): LLMRequest | null {
50
55
  if (this.paused || this.queue.length === 0) return null;
51
- const now = new Date().toISOString();
52
- const available = this.queue.filter(r => !r.retry_after || r.retry_after <= now);
56
+ const available = this.queue.filter(r => r.state === "pending");
53
57
  if (available.length === 0) return null;
54
58
  const priorityOrder = { high: 0, normal: 1, low: 2 };
55
59
  const sorted = [...available].sort(
56
60
  (a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]
57
61
  );
58
- return sorted[0] ?? null;
62
+ const item = sorted[0] ?? null;
63
+ if (item) {
64
+ item.state = "processing";
65
+ }
66
+ return item;
67
+ }
68
+
69
+ // Returns the retry_after of the highest-priority pending item, or null if ready now.
70
+ // Used by the processor loop to decide whether to sleep instead of claiming.
71
+ nextItemRetryAfter(): string | null {
72
+ if (this.paused || this.queue.length === 0) return null;
73
+ const available = this.queue.filter(r => r.state === "pending");
74
+ if (available.length === 0) return null;
75
+ const priorityOrder = { high: 0, normal: 1, low: 2 };
76
+ const sorted = [...available].sort(
77
+ (a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]
78
+ );
79
+ return sorted[0]?.retry_after ?? null;
59
80
  }
60
81
 
61
82
  complete(id: string): void {
@@ -76,38 +97,32 @@ export class QueueState {
76
97
  }
77
98
 
78
99
  // No error string and not flagged permanent = just increment, no classification
100
+ // Still reset to pending in case it was claimed (processing state)
79
101
  if (!error && !permanent) {
102
+ request.state = "pending";
80
103
  return { dropped: false };
81
104
  }
82
105
 
83
106
  const shouldDrop = permanent || (error ? isPermanentError(error) : false);
84
107
 
85
108
  if (shouldDrop) {
86
- this.queue.splice(idx, 1);
109
+ request.state = "dlq";
87
110
  return { dropped: true };
88
111
  }
89
112
 
90
- // Transient error — apply exponential backoff, never drop
113
+ // Transient error — reset to pending with backoff timer
114
+ request.state = "pending";
91
115
  const delay = calculateBackoff(request.attempts);
92
116
  request.retry_after = new Date(Date.now() + delay).toISOString();
93
117
  return { dropped: false, retryDelay: delay };
94
118
  }
95
119
 
96
- getValidations(): LLMRequest[] {
97
- return this.queue.filter(
98
- (r) => r.next_step === ("handleEiValidation" as LLMNextStep)
99
- );
100
- }
101
120
 
102
- clearValidations(ids: string[]): void {
103
- const idSet = new Set(ids);
104
- this.queue = this.queue.filter((r) => !idSet.has(r.id));
105
- }
106
121
 
107
122
  clearPersonaResponses(personaId: string, nextStep: string): string[] {
108
123
  const removedIds: string[] = [];
109
124
  this.queue = this.queue.filter((r) => {
110
- if (r.next_step === nextStep && r.data.personaId === personaId) {
125
+ if (r.state !== "dlq" && r.next_step === nextStep && r.data.personaId === personaId) {
111
126
  removedIds.push(r.id);
112
127
  return false;
113
128
  }
@@ -117,7 +132,49 @@ export class QueueState {
117
132
  }
118
133
 
119
134
  length(): number {
120
- return this.queue.length;
135
+ return this.queue.filter(r => r.state === "pending" || r.state === "processing").length;
136
+ }
137
+
138
+ dlqLength(): number {
139
+ return this.queue.filter(r => r.state === "dlq").length;
140
+ }
141
+
142
+ hasProcessingItem(): boolean {
143
+ return this.queue.some(r => r.state === "processing");
144
+ }
145
+
146
+ getDLQItems(): LLMRequest[] {
147
+ return this.queue.filter(r => r.state === "dlq");
148
+ }
149
+
150
+ getAllActiveItems(): LLMRequest[] {
151
+ return this.queue.filter(r => r.state !== "dlq");
152
+ }
153
+
154
+ updateItem(id: string, updates: Partial<LLMRequest>): boolean {
155
+ const idx = this.queue.findIndex(r => r.id === id);
156
+ if (idx < 0) return false;
157
+ this.queue[idx] = { ...this.queue[idx], ...updates };
158
+ return true;
159
+ }
160
+
161
+ trimDLQ(): number {
162
+ const dlqItems = this.queue.filter(r => r.state === "dlq");
163
+ const cutoff = new Date();
164
+ cutoff.setDate(cutoff.getDate() - DLQ_MAX_AGE_DAYS);
165
+ const cutoffISO = cutoff.toISOString();
166
+
167
+ // Remove by age first
168
+ const afterAgeTrim = dlqItems.filter(r => r.created_at >= cutoffISO);
169
+
170
+ // Then enforce count limit (oldest first)
171
+ const sorted = afterAgeTrim.sort((a, b) => a.created_at.localeCompare(b.created_at));
172
+ const kept = sorted.slice(-DLQ_MAX_COUNT);
173
+ const keptIds = new Set(kept.map(r => r.id));
174
+
175
+ const before = this.queue.length;
176
+ this.queue = this.queue.filter(r => r.state !== "dlq" || keptIds.has(r.id));
177
+ return before - this.queue.length;
121
178
  }
122
179
 
123
180
  pause(): void {
@@ -133,12 +190,12 @@ export class QueueState {
133
190
  }
134
191
 
135
192
  hasPendingCeremonies(): boolean {
136
- return this.queue.some(r => r.data.ceremony_progress === true);
193
+ return this.queue.some(r => r.state !== "dlq" && r.data.ceremony_progress === true);
137
194
  }
138
195
 
139
196
  clear(): number {
140
- const count = this.queue.length;
141
- this.queue = [];
197
+ const count = this.queue.filter(r => r.state !== "dlq").length;
198
+ this.queue = this.queue.filter(r => r.state === "dlq");
142
199
  return count;
143
200
  }
144
201
  }
@@ -250,7 +250,7 @@ export class StateManager {
250
250
  return result;
251
251
  }
252
252
 
253
- queue_enqueue(request: Omit<LLMRequest, "id" | "created_at" | "attempts">): string {
253
+ queue_enqueue(request: Omit<LLMRequest, "id" | "created_at" | "attempts" | "state">): string {
254
254
  const requestWithModel = {
255
255
  ...request,
256
256
  model: request.model ?? this.humanState.get().settings?.default_model,
@@ -260,8 +260,8 @@ export class StateManager {
260
260
  return id;
261
261
  }
262
262
 
263
- queue_peekHighest(): LLMRequest | null {
264
- return this.queueState.peekHighest();
263
+ queue_claimHighest(): LLMRequest | null {
264
+ return this.queueState.claimHighest();
265
265
  }
266
266
 
267
267
  queue_complete(id: string): void {
@@ -275,14 +275,7 @@ export class StateManager {
275
275
  return result;
276
276
  }
277
277
 
278
- queue_getValidations(): LLMRequest[] {
279
- return this.queueState.getValidations();
280
- }
281
278
 
282
- queue_clearValidations(ids: string[]): void {
283
- this.queueState.clearValidations(ids);
284
- this.scheduleSave();
285
- }
286
279
 
287
280
  queue_clearPersonaResponses(personaId: string, nextStep: string): string[] {
288
281
  const result = this.queueState.clearPersonaResponses(personaId, nextStep);
@@ -294,6 +287,14 @@ export class StateManager {
294
287
  return this.queueState.length();
295
288
  }
296
289
 
290
+ queue_hasProcessingItem(): boolean {
291
+ return this.queueState.hasProcessingItem();
292
+ }
293
+
294
+ queue_nextItemRetryAfter(): string | null {
295
+ return this.queueState.nextItemRetryAfter();
296
+ }
297
+
297
298
  queue_pause(): void {
298
299
  this.queueState.pause();
299
300
  this.scheduleSave();
@@ -318,6 +319,30 @@ export class StateManager {
318
319
  return result;
319
320
  }
320
321
 
322
+ queue_dlqLength(): number {
323
+ return this.queueState.dlqLength();
324
+ }
325
+
326
+ queue_getDLQItems(): LLMRequest[] {
327
+ return this.queueState.getDLQItems();
328
+ }
329
+
330
+ queue_getAllActiveItems(): LLMRequest[] {
331
+ return this.queueState.getAllActiveItems();
332
+ }
333
+
334
+ queue_updateItem(id: string, updates: Partial<LLMRequest>): boolean {
335
+ const result = this.queueState.updateItem(id, updates);
336
+ if (result) this.scheduleSave();
337
+ return result;
338
+ }
339
+
340
+ queue_trimDLQ(): number {
341
+ const result = this.queueState.trimDLQ();
342
+ if (result > 0) this.scheduleSave();
343
+ return result;
344
+ }
345
+
321
346
  async flush(): Promise<void> {
322
347
  await this.persistenceState.flush();
323
348
  }
@@ -338,6 +363,7 @@ export class StateManager {
338
363
  this.humanState.load(state.human);
339
364
  this.personaState.load(state.personas);
340
365
  this.queueState.load(state.queue);
366
+ this.persistenceState.markExistingData();
341
367
  this.scheduleSave();
342
368
  }
343
369
 
package/src/core/types.ts CHANGED
@@ -18,7 +18,6 @@ export enum ValidationLevel {
18
18
  Ei = "ei", // Ei mentioned it to user (don't mention again)
19
19
  Human = "human", // User explicitly confirmed (locked)
20
20
  }
21
-
22
21
  export enum LLMRequestType {
23
22
  Response = "response",
24
23
  JSON = "json",
@@ -47,9 +46,7 @@ export enum LLMNextStep {
47
46
  HandlePersonaTopicUpdate = "handlePersonaTopicUpdate",
48
47
  HandleHeartbeatCheck = "handleHeartbeatCheck",
49
48
  HandleEiHeartbeat = "handleEiHeartbeat",
50
- HandleEiValidation = "handleEiValidation",
51
49
  HandleOneShot = "handleOneShot",
52
- // Ceremony handlers
53
50
  HandlePersonaExpire = "handlePersonaExpire",
54
51
  HandlePersonaExplore = "handlePersonaExplore",
55
52
  HandleDescriptionCheck = "handleDescriptionCheck",
@@ -83,6 +80,7 @@ export interface Topic extends DataItemBase {
83
80
  category?: string; // Interest, Goal, Dream, Conflict, Concern, Fear, Hope, Plan, Project
84
81
  exposure_current: number;
85
82
  exposure_desired: number;
83
+ last_ei_asked?: string | null; // ISO timestamp of last time Ei proactively asked about this
86
84
  }
87
85
 
88
86
  /**
@@ -109,6 +107,7 @@ export interface Person extends DataItemBase {
109
107
  relationship: string;
110
108
  exposure_current: number;
111
109
  exposure_desired: number;
110
+ last_ei_asked?: string | null; // ISO timestamp of last time Ei proactively asked about this
112
111
  }
113
112
 
114
113
  export interface Quote {
@@ -180,7 +179,8 @@ export interface OpenCodeSettings {
180
179
  integration?: boolean;
181
180
  polling_interval_ms?: number; // Default: 1800000 (30 min)
182
181
  last_sync?: string; // ISO timestamp
183
- extraction_point?: string; // ISO timestamp - earliest unprocessed message, gradual extraction advances this
182
+ extraction_point?: string; // ISO timestamp - cursor for single-session archive scan
183
+ processed_sessions?: Record<string, string>; // sessionId → ISO timestamp of last import
184
184
  }
185
185
 
186
186
  export interface CeremonyConfig {
@@ -238,7 +238,6 @@ export interface PersonaEntity {
238
238
  last_activity: string;
239
239
  last_heartbeat?: string;
240
240
  last_extraction?: string;
241
- last_inactivity_ping?: string;
242
241
  }
243
242
 
244
243
  export interface PersonaCreationInput {
@@ -257,6 +256,10 @@ export interface PersonaCreationInput {
257
256
  export const MESSAGE_MIN_COUNT = 200;
258
257
  export const MESSAGE_MAX_AGE_DAYS = 14;
259
258
 
259
+ // DLQ rolloff thresholds
260
+ export const DLQ_MAX_COUNT = 50;
261
+ export const DLQ_MAX_AGE_DAYS = 14;
262
+
260
263
  // Reserved persona names (command keywords that conflict with /persona subcommands)
261
264
  export const RESERVED_PERSONA_NAMES = ["new", "clone"] as const;
262
265
  export type ReservedPersonaName = typeof RESERVED_PERSONA_NAMES[number];
@@ -272,14 +275,19 @@ export function isReservedPersonaName(name: string): boolean {
272
275
  export interface Message {
273
276
  id: string;
274
277
  role: "human" | "system";
275
- content: string;
278
+ verbal_response?: string; // Human text or persona's spoken reply
279
+ action_response?: string; // Stage direction / action the persona performs
280
+ silence_reason?: string; // Why the persona chose not to respond (not shown to LLM)
276
281
  timestamp: string;
277
- read: boolean;
282
+ read: boolean; // Has human seen this system message?
278
283
  context_status: ContextStatus;
279
- f?: boolean; // fact extraction done
280
- r?: boolean; // trait extraction done
281
- p?: boolean; // person extraction done
282
- o?: boolean; // topic extraction done
284
+
285
+ // Extraction completion flags (omit when false to save space)
286
+ // Single-letter names minimize storage overhead for large message histories
287
+ f?: boolean; // Fact extraction completed
288
+ r?: boolean; // tRait extraction completed
289
+ p?: boolean; // Person extraction completed
290
+ o?: boolean; // tOpic extraction completed
283
291
  }
284
292
 
285
293
  export interface ChatMessage {
@@ -291,12 +299,15 @@ export interface ChatMessage {
291
299
  // LLM TYPES
292
300
  // =============================================================================
293
301
 
302
+ export type LLMRequestState = "pending" | "processing" | "dlq";
303
+
294
304
  export interface LLMRequest {
295
305
  id: string;
296
306
  created_at: string;
297
307
  attempts: number;
298
308
  last_attempt?: string;
299
309
  retry_after?: string;
310
+ state: LLMRequestState;
300
311
  type: LLMRequestType;
301
312
  priority: LLMPriority;
302
313
  system: string;
@@ -346,6 +357,7 @@ export interface MessageQueryOptions {
346
357
  export interface QueueStatus {
347
358
  state: "idle" | "busy" | "paused";
348
359
  pending_count: number;
360
+ dlq_count: number;
349
361
  current_operation?: string;
350
362
  }
351
363
 
@@ -0,0 +1,44 @@
1
+ import type { HumanEntity, PersonaEntity, Fact, Trait, Topic, Person, Quote, PersonaTopic } from "../types.ts";
2
+ export type CrossFindResult =
3
+ | { type: "fact" } & Fact
4
+ | { type: "trait" } & Trait
5
+ | { type: "topic" } & Topic
6
+ | { type: "person" } & Person
7
+ | { type: "quote" } & Quote
8
+ | { type: "persona" } & PersonaEntity
9
+ | { type: "personaTopic"; personaId: string } & PersonaTopic
10
+ | { type: "personaTrait"; personaId: string } & Trait;
11
+
12
+ export function crossFind(
13
+ id: string,
14
+ human: HumanEntity,
15
+ personas?: PersonaEntity[],
16
+ ): CrossFindResult | null {
17
+
18
+ const fact = human.facts.find(f => f.id === id);
19
+ if (fact) return { type: "fact", ...fact };
20
+
21
+ const trait = human.traits.find(t => t.id === id);
22
+ if (trait) return { type: "trait", ...trait };
23
+
24
+ const person = human.people.find(p => p.id === id);
25
+ if (person) return { type: "person", ...person };
26
+
27
+ const topic = human.topics.find(t => t.id === id);
28
+ if (topic) return { type: "topic", ...topic };
29
+
30
+ const quote = human.quotes.find(q => q.id === id);
31
+ if (quote) return { type: "quote", ...quote };
32
+
33
+ for (const persona of personas ?? []) {
34
+ if (persona.id === id) return { type: "persona", ...persona };
35
+
36
+ const pTopic = persona.topics.find(t => t.id === id);
37
+ if (pTopic) return { type: "personaTopic", personaId: persona.id, ...pTopic };
38
+
39
+ const pTrait = persona.traits.find(t => t.id === id);
40
+ if (pTrait) return { type: "personaTrait", personaId: persona.id, ...pTrait };
41
+ }
42
+
43
+ return null;
44
+ }
@@ -0,0 +1,4 @@
1
+ import { crossFind } from "./crossFind.js";
2
+ import { applyDecayToValue } from "./decay.js";
3
+
4
+ export { crossFind, applyDecayToValue };