ei-tui 0.4.3 → 0.5.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.
Files changed (102) hide show
  1. package/README.md +14 -0
  2. package/package.json +1 -1
  3. package/src/cli/README.md +17 -12
  4. package/src/cli/commands/personas.ts +12 -0
  5. package/src/cli/mcp.ts +2 -2
  6. package/src/cli/retrieval.ts +86 -8
  7. package/src/cli.ts +8 -5
  8. package/src/core/constants/seed-traits.ts +29 -0
  9. package/src/core/context-utils.ts +1 -0
  10. package/src/core/handlers/human-matching.ts +86 -56
  11. package/src/core/handlers/index.ts +5 -0
  12. package/src/core/handlers/persona-preview.ts +7 -0
  13. package/src/core/handlers/persona-topics.ts +3 -2
  14. package/src/core/handlers/rooms.ts +176 -0
  15. package/src/core/handlers/utils.ts +55 -3
  16. package/src/core/heartbeat-manager.ts +3 -1
  17. package/src/core/llm-client.ts +1 -1
  18. package/src/core/message-manager.ts +10 -8
  19. package/src/core/orchestrators/human-extraction.ts +15 -2
  20. package/src/core/orchestrators/index.ts +1 -0
  21. package/src/core/orchestrators/persona-generation.ts +4 -0
  22. package/src/core/orchestrators/persona-topics.ts +2 -1
  23. package/src/core/orchestrators/room-extraction.ts +318 -0
  24. package/src/core/persona-manager.ts +16 -5
  25. package/src/core/personas/opencode-agent.ts +12 -2
  26. package/src/core/processor.ts +520 -4
  27. package/src/core/prompt-context-builder.ts +89 -5
  28. package/src/core/queue-processor.ts +68 -8
  29. package/src/core/room-manager.ts +408 -0
  30. package/src/core/state/index.ts +1 -0
  31. package/src/core/state/personas.ts +12 -2
  32. package/src/core/state/queue.ts +2 -2
  33. package/src/core/state/rooms.ts +182 -0
  34. package/src/core/state-manager.ts +124 -2
  35. package/src/core/tool-manager.ts +1 -1
  36. package/src/core/tools/index.ts +15 -0
  37. package/src/core/types/data-items.ts +3 -1
  38. package/src/core/types/enums.ts +11 -0
  39. package/src/core/types/integrations.ts +10 -2
  40. package/src/core/types/llm.ts +3 -0
  41. package/src/core/types/rooms.ts +59 -0
  42. package/src/core/types.ts +1 -0
  43. package/src/core/utils/decay.ts +14 -8
  44. package/src/core/utils/exposure.ts +14 -0
  45. package/src/integrations/claude-code/importer.ts +23 -10
  46. package/src/integrations/cursor/importer.ts +22 -10
  47. package/src/integrations/opencode/importer.ts +30 -13
  48. package/src/prompts/ceremony/dedup.ts +2 -2
  49. package/src/prompts/generation/from-person.ts +85 -0
  50. package/src/prompts/generation/index.ts +2 -0
  51. package/src/prompts/generation/persona.ts +14 -10
  52. package/src/prompts/generation/seeds.ts +4 -29
  53. package/src/prompts/generation/types.ts +13 -0
  54. package/src/prompts/heartbeat/check.ts +1 -1
  55. package/src/prompts/heartbeat/ei.ts +4 -4
  56. package/src/prompts/heartbeat/types.ts +1 -0
  57. package/src/prompts/index.ts +15 -0
  58. package/src/prompts/message-utils.ts +2 -2
  59. package/src/prompts/persona/topics-match.ts +7 -6
  60. package/src/prompts/persona/topics-update.ts +8 -11
  61. package/src/prompts/persona/types.ts +2 -1
  62. package/src/prompts/response/index.ts +1 -1
  63. package/src/prompts/response/sections.ts +20 -8
  64. package/src/prompts/response/types.ts +6 -0
  65. package/src/prompts/room/index.ts +115 -0
  66. package/src/prompts/room/sections.ts +150 -0
  67. package/src/prompts/room/types.ts +93 -0
  68. package/tui/README.md +20 -0
  69. package/tui/src/app.tsx +3 -2
  70. package/tui/src/commands/activate.tsx +98 -0
  71. package/tui/src/commands/archive.tsx +54 -25
  72. package/tui/src/commands/capture.tsx +50 -0
  73. package/tui/src/commands/dedupe.tsx +2 -7
  74. package/tui/src/commands/delete.tsx +48 -0
  75. package/tui/src/commands/details.tsx +7 -0
  76. package/tui/src/commands/persona.tsx +271 -9
  77. package/tui/src/commands/room.tsx +261 -0
  78. package/tui/src/commands/silence.tsx +29 -0
  79. package/tui/src/components/ArchivedItemsOverlay.tsx +144 -0
  80. package/tui/src/components/ConfirmOverlay.tsx +6 -0
  81. package/tui/src/components/ConflictOverlay.tsx +6 -0
  82. package/tui/src/components/HelpOverlay.tsx +6 -1
  83. package/tui/src/components/LoadingOverlay.tsx +51 -0
  84. package/tui/src/components/MessageList.tsx +1 -18
  85. package/tui/src/components/PersonPickerOverlay.tsx +121 -0
  86. package/tui/src/components/PersonaListOverlay.tsx +6 -1
  87. package/tui/src/components/PromptInput.tsx +141 -8
  88. package/tui/src/components/ProviderListOverlay.tsx +5 -1
  89. package/tui/src/components/QuotesOverlay.tsx +5 -1
  90. package/tui/src/components/RoomMessageList.tsx +179 -0
  91. package/tui/src/components/Sidebar.tsx +54 -2
  92. package/tui/src/components/StatusBar.tsx +99 -8
  93. package/tui/src/components/ToolkitListOverlay.tsx +5 -1
  94. package/tui/src/components/WelcomeOverlay.tsx +6 -0
  95. package/tui/src/context/ei.tsx +252 -1
  96. package/tui/src/context/keyboard.tsx +48 -12
  97. package/tui/src/util/cyp-editor.tsx +152 -0
  98. package/tui/src/util/quote-utils.ts +19 -0
  99. package/tui/src/util/room-editor.tsx +164 -0
  100. package/tui/src/util/room-logic.ts +8 -0
  101. package/tui/src/util/room-parser.ts +70 -0
  102. package/tui/src/util/yaml-serializers.ts +151 -0
@@ -213,10 +213,20 @@ export class PersonaState {
213
213
  return removed;
214
214
  }
215
215
 
216
- messages_getUnextracted(personaId: string, flag: "f" | "t" | "p" | "e", limit?: number): Message[] {
216
+ messages_getUnextracted(
217
+ personaId: string,
218
+ flag: "f" | "t" | "p" | "e",
219
+ limit?: number,
220
+ external_filter?: "include" | "exclude" | "only"
221
+ ): Message[] {
217
222
  const data = this.personas.get(personaId);
218
223
  if (!data) return [];
219
- const unextracted = data.messages.filter(m => m[flag] !== true);
224
+ let unextracted = data.messages.filter(m => m[flag] !== true);
225
+ if (external_filter === "exclude") {
226
+ unextracted = unextracted.filter(m => m.external !== true);
227
+ } else if (external_filter === "only") {
228
+ unextracted = unextracted.filter(m => m.external === true);
229
+ }
220
230
  if (limit && unextracted.length > limit) {
221
231
  return unextracted.slice(0, limit).map(m => ({ ...m }));
222
232
  }
@@ -55,7 +55,7 @@ export class QueueState {
55
55
  if (this.paused || this.queue.length === 0) return null;
56
56
  const available = this.queue.filter(r => r.state === "pending");
57
57
  if (available.length === 0) return null;
58
- const priorityOrder = { high: 0, normal: 1, low: 2 };
58
+ const priorityOrder = { high: 0, judge: 1, room: 2, normal: 3, low: 4 };
59
59
  const sorted = [...available].sort(
60
60
  (a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]
61
61
  );
@@ -72,7 +72,7 @@ export class QueueState {
72
72
  if (this.paused || this.queue.length === 0) return null;
73
73
  const available = this.queue.filter(r => r.state === "pending");
74
74
  if (available.length === 0) return null;
75
- const priorityOrder = { high: 0, normal: 1, low: 2 };
75
+ const priorityOrder = { high: 0, judge: 1, room: 2, normal: 3, low: 4 };
76
76
  const sorted = [...available].sort(
77
77
  (a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]
78
78
  );
@@ -0,0 +1,182 @@
1
+ /**
2
+ * EI V1 Room State
3
+ * Source of truth: CONTRACTS.md
4
+ */
5
+
6
+ import type { RoomEntity, RoomMessage, RoomSummary } from "../types.js";
7
+
8
+ export class RoomState {
9
+ private rooms: Map<string, RoomEntity> = new Map();
10
+
11
+ load(rooms: Record<string, RoomEntity> | undefined): void {
12
+ if (!rooms) return;
13
+ this.rooms = new Map(Object.entries(rooms));
14
+ }
15
+
16
+ export(): Record<string, RoomEntity> {
17
+ const result: Record<string, RoomEntity> = {};
18
+ for (const [id, room] of this.rooms) {
19
+ result[id] = room;
20
+ }
21
+ return result;
22
+ }
23
+
24
+ getAll(includeArchived = false): RoomEntity[] {
25
+ const all = Array.from(this.rooms.values());
26
+ return includeArchived ? all : all.filter(r => !r.is_archived);
27
+ }
28
+
29
+ getById(id: string): RoomEntity | null {
30
+ return this.rooms.get(id) ?? null;
31
+ }
32
+
33
+ getByName(nameOrAlias: string): RoomEntity | null {
34
+ const search = nameOrAlias.toLowerCase();
35
+ for (const room of this.rooms.values()) {
36
+ if (room.display_name.toLowerCase() === search) return room;
37
+ }
38
+ const partial: RoomEntity[] = [];
39
+ for (const room of this.rooms.values()) {
40
+ if (room.display_name.toLowerCase().includes(search)) partial.push(room);
41
+ }
42
+ return partial.length === 1 ? partial[0] : null;
43
+ }
44
+
45
+ add(room: RoomEntity): void {
46
+ this.rooms.set(room.id, room);
47
+ }
48
+
49
+ update(roomId: string, updates: Partial<RoomEntity>): boolean {
50
+ const room = this.rooms.get(roomId);
51
+ if (!room) return false;
52
+ Object.assign(room, updates, { last_updated: new Date().toISOString() });
53
+ return true;
54
+ }
55
+
56
+ archive(roomId: string): boolean {
57
+ const room = this.rooms.get(roomId);
58
+ if (!room) return false;
59
+ room.is_archived = true;
60
+ room.last_updated = new Date().toISOString();
61
+ return true;
62
+ }
63
+
64
+ delete(roomId: string): boolean {
65
+ return this.rooms.delete(roomId);
66
+ }
67
+
68
+ getSummary(roomId: string): RoomSummary | null {
69
+ const room = this.rooms.get(roomId);
70
+ if (!room) return null;
71
+ return {
72
+ id: room.id,
73
+ display_name: room.display_name,
74
+ mode: room.mode,
75
+ persona_ids: room.persona_ids,
76
+ active_node_id: room.active_node_id,
77
+ is_archived: room.is_archived,
78
+ last_activity: room.last_activity,
79
+ unread_count: this.messages_countUnread(roomId),
80
+ };
81
+ }
82
+
83
+ messages_get(roomId: string): RoomMessage[] {
84
+ return this.rooms.get(roomId)?.messages ?? [];
85
+ }
86
+
87
+ messages_getActivePath(roomId: string): RoomMessage[] {
88
+ const room = this.rooms.get(roomId);
89
+ if (!room || !room.active_node_id) return [];
90
+
91
+ const byId = new Map(room.messages.map(m => [m.id, m]));
92
+ const path: RoomMessage[] = [];
93
+ let current: RoomMessage | undefined = byId.get(room.active_node_id);
94
+ while (current) {
95
+ path.unshift(current);
96
+ current = current.parent_id ? byId.get(current.parent_id) : undefined;
97
+ }
98
+ return path;
99
+ }
100
+
101
+ messages_getChildren(roomId: string, parentId: string | null): RoomMessage[] {
102
+ const room = this.rooms.get(roomId);
103
+ if (!room) return [];
104
+ return room.messages.filter(m => m.parent_id === parentId);
105
+ }
106
+
107
+ messages_append(roomId: string, message: RoomMessage): void {
108
+ const room = this.rooms.get(roomId);
109
+ if (!room) return;
110
+ room.messages.push(message);
111
+ room.last_activity = message.timestamp;
112
+ room.last_updated = new Date().toISOString();
113
+ }
114
+
115
+ messages_update(roomId: string, messageId: string, updates: Partial<RoomMessage>): boolean {
116
+ const room = this.rooms.get(roomId);
117
+ if (!room) return false;
118
+ const msg = room.messages.find(m => m.id === messageId);
119
+ if (!msg) return false;
120
+ Object.assign(msg, updates);
121
+ room.last_updated = new Date().toISOString();
122
+ return true;
123
+ }
124
+
125
+ messages_remove(roomId: string, messageIds: string[]): void {
126
+ const room = this.rooms.get(roomId);
127
+ if (!room) return;
128
+ const idsSet = new Set(messageIds);
129
+ room.messages = room.messages.filter(m => !idsSet.has(m.id));
130
+ room.last_updated = new Date().toISOString();
131
+ }
132
+
133
+ messages_setActiveNode(roomId: string, messageId: string): boolean {
134
+ const room = this.rooms.get(roomId);
135
+ if (!room) return false;
136
+ const exists = room.messages.some(m => m.id === messageId);
137
+ if (!exists) return false;
138
+ room.active_node_id = messageId;
139
+ room.last_updated = new Date().toISOString();
140
+ return true;
141
+ }
142
+
143
+ messages_countUnread(roomId: string): number {
144
+ const room = this.rooms.get(roomId);
145
+ if (!room) return 0;
146
+ const activePath = new Set(this.messages_getActivePath(roomId).map(m => m.id));
147
+ return room.messages.filter(m =>
148
+ m.role === "persona" && !m.read && activePath.has(m.id)
149
+ ).length;
150
+ }
151
+
152
+ messages_markAllRead(roomId: string): number {
153
+ const room = this.rooms.get(roomId);
154
+ if (!room) return 0;
155
+ let count = 0;
156
+ for (const msg of room.messages) {
157
+ if (!msg.read) { msg.read = true; count++; }
158
+ }
159
+ return count;
160
+ }
161
+
162
+ messages_getUnextracted(roomId: string, flag: "f" | "t" | "p" | "e"): RoomMessage[] {
163
+ const activePath = new Set(this.messages_getActivePath(roomId).map(m => m.id));
164
+ return (this.rooms.get(roomId)?.messages ?? [])
165
+ .filter(m => activePath.has(m.id) && m[flag] !== true)
166
+ .map(m => ({ ...m }));
167
+ }
168
+
169
+ messages_markExtracted(roomId: string, messageIds: string[], flag: "f" | "t" | "p" | "e"): number {
170
+ const room = this.rooms.get(roomId);
171
+ if (!room) return 0;
172
+ const ids = new Set(messageIds);
173
+ let count = 0;
174
+ for (const msg of room.messages) {
175
+ if (ids.has(msg.id) && msg[flag] !== true) {
176
+ msg[flag] = true;
177
+ count++;
178
+ }
179
+ }
180
+ return count;
181
+ }
182
+ }
@@ -12,6 +12,10 @@ import type {
12
12
  QueueFailResult,
13
13
  ToolDefinition,
14
14
  ToolProvider,
15
+ RoomEntity,
16
+ RoomMessage,
17
+ RoomSummary,
18
+ RoomCreationInput,
15
19
  } from "./types.js";
16
20
  import { BUILT_IN_FACT_NAMES } from './constants/built-in-facts.js';
17
21
  import type { Storage } from "../storage/interface.js";
@@ -20,12 +24,14 @@ import {
20
24
  PersonaState,
21
25
  QueueState,
22
26
  PersistenceState,
27
+ RoomState,
23
28
  createDefaultHumanEntity,
24
29
  } from "./state/index.js";
25
30
 
26
31
  export class StateManager {
27
32
  private humanState = new HumanState();
28
33
  private personaState = new PersonaState();
34
+ private roomState = new RoomState();
29
35
  private queueState = new QueueState();
30
36
  private persistenceState = new PersistenceState();
31
37
  private providers: ToolProvider[] = [];
@@ -40,6 +46,7 @@ export class StateManager {
40
46
  if (state) {
41
47
  this.humanState.load(state.human);
42
48
  this.personaState.load(state.personas);
49
+ this.roomState.load(state.rooms);
43
50
  this.queueState.load(state.queue);
44
51
  this.tools = state.tools ?? [];
45
52
  this.providers = state.providers ?? [];
@@ -230,12 +237,126 @@ export class StateManager {
230
237
  timestamp: new Date().toISOString(),
231
238
  human: this.humanState.get(),
232
239
  personas: this.personaState.export(),
240
+ rooms: this.roomState.export(),
233
241
  queue: this.queueState.export(),
234
242
  providers: this.providers,
235
243
  tools: this.tools,
236
244
  };
237
245
  }
238
246
 
247
+ getRoomList(includeArchived = false): RoomSummary[] {
248
+ return this.roomState.getAll(includeArchived)
249
+ .map(r => this.roomState.getSummary(r.id)!)
250
+ .sort((a, b) => new Date(b.last_activity).getTime() - new Date(a.last_activity).getTime());
251
+ }
252
+
253
+ getRoom(roomId: string): RoomEntity | null {
254
+ return this.roomState.getById(roomId);
255
+ }
256
+
257
+ getRoomByName(name: string): RoomEntity | null {
258
+ return this.roomState.getByName(name);
259
+ }
260
+
261
+ addRoom(input: RoomCreationInput): RoomEntity {
262
+ const now = new Date().toISOString();
263
+ const initialMessage: RoomMessage = {
264
+ id: crypto.randomUUID(),
265
+ parent_id: null,
266
+ role: "human",
267
+ verbal_response: input.initial_message,
268
+ timestamp: now,
269
+ read: true,
270
+ context_status: "default" as import("./types.js").ContextStatus,
271
+ };
272
+ const room: RoomEntity = {
273
+ id: crypto.randomUUID(),
274
+ display_name: input.display_name,
275
+ entity: "room",
276
+ mode: input.mode,
277
+ persona_ids: input.persona_ids,
278
+ judge_persona_id: input.judge_persona_id,
279
+ active_node_id: initialMessage.id,
280
+ is_archived: false,
281
+ created_at: now,
282
+ last_updated: now,
283
+ last_activity: now,
284
+ messages: [initialMessage],
285
+ };
286
+ this.roomState.add(room);
287
+ this.scheduleSave();
288
+ return room;
289
+ }
290
+
291
+ updateRoom(roomId: string, updates: Partial<RoomEntity>): boolean {
292
+ const ok = this.roomState.update(roomId, updates);
293
+ if (ok) this.scheduleSave();
294
+ return ok;
295
+ }
296
+
297
+ archiveRoom(roomId: string): boolean {
298
+ const ok = this.roomState.archive(roomId);
299
+ if (ok) this.scheduleSave();
300
+ return ok;
301
+ }
302
+
303
+ deleteRoom(roomId: string): boolean {
304
+ const ok = this.roomState.delete(roomId);
305
+ if (ok) this.scheduleSave();
306
+ return ok;
307
+ }
308
+
309
+ getRoomMessages(roomId: string): RoomMessage[] {
310
+ return this.roomState.messages_get(roomId);
311
+ }
312
+
313
+ getRoomActivePath(roomId: string): RoomMessage[] {
314
+ return this.roomState.messages_getActivePath(roomId);
315
+ }
316
+
317
+ getRoomChildren(roomId: string, parentId: string | null): RoomMessage[] {
318
+ return this.roomState.messages_getChildren(roomId, parentId);
319
+ }
320
+
321
+ appendRoomMessage(roomId: string, message: RoomMessage): void {
322
+ this.roomState.messages_append(roomId, message);
323
+ this.scheduleSave();
324
+ }
325
+
326
+ updateRoomMessage(roomId: string, messageId: string, updates: Partial<RoomMessage>): boolean {
327
+ const ok = this.roomState.messages_update(roomId, messageId, updates);
328
+ if (ok) this.scheduleSave();
329
+ return ok;
330
+ }
331
+
332
+ setRoomActiveNode(roomId: string, messageId: string): boolean {
333
+ const ok = this.roomState.messages_setActiveNode(roomId, messageId);
334
+ if (ok) this.scheduleSave();
335
+ return ok;
336
+ }
337
+
338
+ removeRoomMessages(roomId: string, messageIds: string[]): void {
339
+ if (messageIds.length === 0) return;
340
+ this.roomState.messages_remove(roomId, messageIds);
341
+ this.scheduleSave();
342
+ }
343
+
344
+ markAllRoomMessagesRead(roomId: string): number {
345
+ const count = this.roomState.messages_markAllRead(roomId);
346
+ if (count > 0) this.scheduleSave();
347
+ return count;
348
+ }
349
+
350
+ getRoomUnextractedMessages(roomId: string, flag: "f" | "t" | "p" | "e"): RoomMessage[] {
351
+ return this.roomState.messages_getUnextracted(roomId, flag);
352
+ }
353
+
354
+ markRoomMessagesExtracted(roomId: string, messageIds: string[], flag: "f" | "t" | "p" | "e"): number {
355
+ const count = this.roomState.messages_markExtracted(roomId, messageIds, flag);
356
+ if (count > 0) this.scheduleSave();
357
+ return count;
358
+ }
359
+
239
360
  private scheduleSave(): void {
240
361
  this.persistenceState.scheduleSave(this.buildStorageState());
241
362
  }
@@ -421,8 +542,8 @@ export class StateManager {
421
542
  return result;
422
543
  }
423
544
 
424
- messages_getUnextracted(personaId: string, flag: "f" | "t" | "p" | "e", limit?: number): Message[] {
425
- return this.personaState.messages_getUnextracted(personaId, flag, limit);
545
+ messages_getUnextracted(personaId: string, flag: "f" | "t" | "p" | "e", limit?: number, external_filter?: "include" | "exclude" | "only"): Message[] {
546
+ return this.personaState.messages_getUnextracted(personaId, flag, limit, external_filter);
426
547
  }
427
548
 
428
549
  messages_markExtracted(personaId: string, messageIds: string[], flag: "f" | "t" | "p" | "e"): number {
@@ -662,6 +783,7 @@ export class StateManager {
662
783
  restoreFromState(state: StorageState): void {
663
784
  this.humanState.load(state.human);
664
785
  this.personaState.load(state.personas);
786
+ this.roomState.load(state.rooms);
665
787
  this.queueState.load(state.queue);
666
788
  this.providers = state.providers ?? [];
667
789
  this.tools = state.tools ?? [];
@@ -54,7 +54,7 @@ export async function removeToolProvider(sm: StateManager, id: string): Promise<
54
54
  // =============================================================================
55
55
 
56
56
  export function getToolList(sm: StateManager): ToolDefinition[] {
57
- return sm.tools_getAll();
57
+ return sm.tools_getAll().filter(t => !t.is_submit);
58
58
  }
59
59
 
60
60
  export function getTool(sm: StateManager, id: string): ToolDefinition | null {
@@ -81,10 +81,25 @@ export function toOpenAITools(tools: ToolDefinition[]): Record<string, unknown>[
81
81
  name: t.name,
82
82
  description: t.description,
83
83
  parameters: t.input_schema,
84
+ ...(t.is_submit ? { strict: true } : {}),
84
85
  },
85
86
  }));
86
87
  }
87
88
 
89
+ /**
90
+ * Returns the first tool call in the batch that maps to an is_submit tool, or undefined.
91
+ * When a submit tool is called, its arguments ARE the structured response — no execution needed.
92
+ */
93
+ export function findSubmitToolCall(
94
+ toolCalls: ToolCall[],
95
+ activeTools: ToolDefinition[]
96
+ ): ToolCall | undefined {
97
+ const submitNames = new Set(
98
+ activeTools.filter(t => t.is_submit).map(t => t.name)
99
+ );
100
+ return toolCalls.find(call => submitNames.has(call.name));
101
+ }
102
+
88
103
  // =============================================================================
89
104
  // Tool call execution
90
105
  // =============================================================================
@@ -67,7 +67,9 @@ export interface Quote {
67
67
  data_item_ids: string[]; // FK[] to DataItemBase.id
68
68
  persona_groups: string[]; // Visibility groups
69
69
  text: string; // The quote content
70
- speaker: "human" | string; // Who said it (persona ID or "human")
70
+ speaker: "human" | string; // Actual speaker: "human" or the persona's display_name
71
+ channel?: string; // Display name of the Channel (persona or room) where captured.
72
+ // Undefined on pre-migration quotes.
71
73
  timestamp: string; // ISO timestamp (from original message)
72
74
  start: number | null; // Character offset in message (null = can't highlight)
73
75
  end: number | null; // Character offset in message (null = can't highlight)
@@ -17,6 +17,8 @@ export enum LLMRequestType {
17
17
 
18
18
  export enum LLMPriority {
19
19
  High = "high",
20
+ Judge = "judge",
21
+ Room = "room",
20
22
  Normal = "normal",
21
23
  Low = "low",
22
24
  }
@@ -51,6 +53,9 @@ export enum LLMNextStep {
51
53
  HandleRewriteRewrite = "handleRewriteRewrite",
52
54
  HandleDedupCurate = "handleDedupCurate",
53
55
  HandleEventScan = "handleEventScan",
56
+ HandleRoomResponse = "handleRoomResponse",
57
+ HandleRoomJudge = "handleRoomJudge",
58
+ HandlePersonaPreview = "handlePersonaPreview",
54
59
  }
55
60
 
56
61
  export enum ProviderType {
@@ -58,3 +63,9 @@ export enum ProviderType {
58
63
  Storage = "storage",
59
64
  Image = "image",
60
65
  }
66
+
67
+ export enum RoomMode {
68
+ ChooseYourPath = "choose_your_path",
69
+ FreeForAll = "free_for_all",
70
+ MessagesAgainstPersona = "messages_against_persona",
71
+ }
@@ -39,6 +39,7 @@ export interface ToolDefinition {
39
39
  enabled: boolean;
40
40
  created_at: string; // ISO timestamp
41
41
  max_calls_per_interaction?: number; // Max times LLM may call this tool per response turn. Default: 3.
42
+ is_submit?: boolean; // If true, calling this tool IS the structured response — terminates the tool loop immediately and its arguments become response.parsed
42
43
  }
43
44
 
44
45
  // =============================================================================
@@ -108,6 +109,12 @@ export interface Ei_Interface {
108
109
  onToolAdded?: () => void;
109
110
  onToolUpdated?: (id: string) => void;
110
111
  onToolRemoved?: () => void;
112
+ onRoomAdded?: () => void;
113
+ onRoomRemoved?: () => void;
114
+ onRoomUpdated?: (roomId: string) => void;
115
+ onRoomMessageAdded?: (roomId: string) => void;
116
+ onRoomMessageQueued?: (roomId: string) => void;
117
+ onRoomMessageProcessing?: (roomId: string) => void;
111
118
  }
112
119
 
113
120
  // =============================================================================
@@ -137,7 +144,8 @@ export interface StorageState {
137
144
  messages: Message[];
138
145
  }
139
146
  >;
147
+ rooms?: Record<string, import("./rooms.js").RoomEntity>;
140
148
  queue: LLMRequest[];
141
- providers: ToolProvider[]; // Tool provider registry (Ei, Brave, etc.)
142
- tools: ToolDefinition[]; // Platform-level tool registry
149
+ providers: ToolProvider[];
150
+ tools: ToolDefinition[];
143
151
  }
@@ -23,6 +23,9 @@ export interface Message {
23
23
  e?: boolean; // Event (epic) extraction completed
24
24
  // Image generation fields (web-only, ephemeral)
25
25
  _synthesis?: boolean; // True if message was created by multi-message synthesis
26
+ speaker_name?: string; // Display name of actual speaker; set on room messages for clean hydration
27
+
28
+ external?: boolean; // Set by integration importers (OpenCode, Cursor, Claude Code); invisible to LLM context
26
29
 
27
30
  }
28
31
 
@@ -0,0 +1,59 @@
1
+ /**
2
+ * EI V1 Room Types
3
+ * Source of truth: CONTRACTS.md
4
+ */
5
+
6
+ import type { RoomMode, ContextStatus } from "./enums.js";
7
+
8
+ export interface RoomMessage {
9
+ id: string;
10
+ parent_id: string | null;
11
+ role: "human" | "persona";
12
+ persona_id?: string;
13
+ verbal_response?: string;
14
+ action_response?: string;
15
+ silence_reason?: string;
16
+ timestamp: string;
17
+ read: boolean;
18
+ context_status: ContextStatus;
19
+
20
+ f?: boolean;
21
+ t?: boolean;
22
+ p?: boolean;
23
+ e?: boolean;
24
+ }
25
+
26
+ export interface RoomEntity {
27
+ id: string;
28
+ display_name: string;
29
+ entity: "room";
30
+ mode: RoomMode;
31
+ persona_ids: string[];
32
+ judge_persona_id?: string;
33
+ active_node_id: string | null;
34
+ is_archived: boolean;
35
+ created_at: string;
36
+ last_updated: string;
37
+ last_activity: string;
38
+ capture_used?: boolean;
39
+ messages: RoomMessage[];
40
+ }
41
+
42
+ export interface RoomCreationInput {
43
+ display_name: string;
44
+ mode: RoomMode;
45
+ persona_ids: string[];
46
+ judge_persona_id?: string;
47
+ initial_message: string;
48
+ }
49
+
50
+ export interface RoomSummary {
51
+ id: string;
52
+ display_name: string;
53
+ mode: RoomMode;
54
+ persona_ids: string[];
55
+ active_node_id: string | null;
56
+ is_archived: boolean;
57
+ last_activity: string;
58
+ unread_count: number;
59
+ }
package/src/core/types.ts CHANGED
@@ -11,3 +11,4 @@ export * from "./types/data-items.js";
11
11
  export * from "./types/entities.js";
12
12
  export * from "./types/llm.js";
13
13
  export * from "./types/integrations.js";
14
+ export * from "./types/rooms.js";
@@ -1,18 +1,24 @@
1
1
  /**
2
- * Logarithmic decay utility for exposure values.
3
- * Ported from V0: v0/src/topic-decay.ts
2
+ * Exponential decay utility for exposure values.
4
3
  *
5
- * The formula decays faster at extremes (0 and 1), slower in the middle.
6
- * K=0.1 means ~10% decay per day at midpoint (0.5).
4
+ * Formula: v(t) = v₀ · e^(-K · days)
5
+ *
6
+ * K=0.1 means ~9.5% decay per day regardless of current value.
7
+ * Decays fastest immediately after peak, slows as it approaches 0.
8
+ * A topic at 1.0 reaches ~0.5 after ~7 days, ~0.05 after ~30 days.
9
+ *
10
+ * This replaced the old logistic approximation (K * v * (1-v) * hours)
11
+ * which had the wrong shape: it decayed FASTEST at 0.5, not at 1.0,
12
+ * and was aggressive enough to drop 0.2 → 0 in a single day.
7
13
  */
8
14
 
9
- export function calculateLogarithmicDecay(
15
+ export function calculateExponentialDecay(
10
16
  currentValue: number,
11
17
  hoursSinceUpdate: number,
12
18
  K: number = 0.1
13
19
  ): number {
14
- const decay = K * currentValue * (1 - currentValue) * hoursSinceUpdate;
15
- return Math.max(0, Math.min(1, currentValue - decay));
20
+ const days = hoursSinceUpdate / 24;
21
+ return Math.max(0, Math.min(1, currentValue * Math.exp(-K * days)));
16
22
  }
17
23
 
18
24
  export function applyDecayToValue(
@@ -28,6 +34,6 @@ export function applyDecayToValue(
28
34
  return { newValue: currentValue, hoursSinceUpdate };
29
35
  }
30
36
 
31
- const newValue = calculateLogarithmicDecay(currentValue, hoursSinceUpdate, K);
37
+ const newValue = calculateExponentialDecay(currentValue, hoursSinceUpdate, K);
32
38
  return { newValue, hoursSinceUpdate };
33
39
  }
@@ -0,0 +1,14 @@
1
+ import type { ExposureImpact } from "../../prompts/human/types.js";
2
+
3
+ export function calculateExposureCurrent(impact: ExposureImpact | undefined, current: number = 0): number {
4
+ const target = (() => {
5
+ switch (impact) {
6
+ case "high": return 0.9;
7
+ case "medium": return 0.6;
8
+ case "low": return 0.3;
9
+ case "none": return 0.1;
10
+ default: return 0.5;
11
+ }
12
+ })();
13
+ return Math.max(target, current);
14
+ }