ei-tui 0.8.1 → 0.9.0
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/README.md +9 -6
- package/package.json +1 -1
- package/src/core/handlers/heartbeat.ts +63 -8
- package/src/core/handlers/index.ts +2 -1
- package/src/core/handlers/persona-response.ts +3 -5
- package/src/core/handlers/rooms.ts +3 -5
- package/src/core/handlers/utils.ts +5 -4
- package/src/core/heartbeat-manager.ts +16 -47
- package/src/core/message-manager.ts +6 -2
- package/src/core/orchestrators/ceremony.ts +49 -4
- package/src/core/persona-manager.ts +1 -2
- package/src/core/personas/opencode-agent.ts +7 -2
- package/src/core/processor.ts +5 -12
- package/src/core/prompt-context-builder.ts +11 -1
- package/src/core/queue-processor.ts +4 -4
- package/src/core/room-manager.ts +6 -6
- package/src/core/state/human.ts +0 -1
- package/src/core/state/personas.ts +22 -13
- package/src/core/state/rooms.ts +0 -2
- package/src/core/state-manager.ts +83 -11
- package/src/core/types/data-items.ts +2 -2
- package/src/core/types/entities.ts +8 -3
- package/src/core/types/enums.ts +1 -0
- package/src/core/types/integrations.ts +1 -1
- package/src/core/types/llm.ts +2 -4
- package/src/core/types/rooms.ts +0 -4
- package/src/integrations/claude-code/importer.ts +1 -5
- package/src/integrations/cursor/importer.ts +1 -5
- package/src/integrations/opencode/importer.ts +1 -4
- package/src/integrations/opencode/types.ts +17 -1
- package/src/prompts/heartbeat/check.ts +7 -18
- package/src/prompts/heartbeat/ei.ts +14 -0
- package/src/prompts/heartbeat/types.ts +7 -5
- package/src/prompts/index.ts +9 -0
- package/src/prompts/message-utils.ts +7 -4
- package/src/prompts/reflection/index.ts +77 -0
- package/src/prompts/reflection/types.ts +26 -0
- package/src/prompts/response/index.ts +5 -2
- package/src/prompts/response/sections.ts +29 -1
- package/src/prompts/response/types.ts +10 -2
- package/src/prompts/room/sections.ts +4 -7
- package/src/prompts/room/types.ts +3 -6
- package/src/storage/embeddings.ts +69 -34
- package/src/storage/merge.ts +1 -1
- package/src/templates/welcome.ts +0 -1
- package/tui/README.md +5 -2
- package/tui/src/commands/editor.tsx +0 -1
- package/tui/src/commands/persona.tsx +89 -3
- package/tui/src/commands/reflect.tsx +375 -0
- package/tui/src/commands/registry.ts +2 -0
- package/tui/src/components/CYPTreeOverlay.tsx +0 -2
- package/tui/src/components/MAPScoreOverlay.tsx +1 -1
- package/tui/src/components/MessageList.tsx +6 -9
- package/tui/src/components/PromptInput.tsx +3 -1
- package/tui/src/components/RoomMessageList.tsx +2 -6
- package/tui/src/components/Sidebar.tsx +3 -5
- package/tui/src/components/StatusBar.tsx +26 -14
- package/tui/src/context/keyboard.tsx +2 -2
- package/tui/src/util/cyp-editor.tsx +2 -6
- package/tui/src/util/yaml-context.ts +2 -6
- package/tui/src/util/yaml-persona.ts +3 -3
- package/tui/src/util/yaml-settings.ts +0 -3
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import type { PersonaEntity, Message, ContextStatus } from "../types.js";
|
|
2
2
|
|
|
3
|
+
// TODO(v1.0.0): Remove LegacyMessage migration — verbal_response/action_response no longer written
|
|
4
|
+
type LegacyMessage = Message & { verbal_response?: string; action_response?: string };
|
|
5
|
+
|
|
3
6
|
function migrateMessage(msg: Message): Message {
|
|
4
7
|
if (msg.content) return msg;
|
|
5
|
-
if (msg.role === 'human') return msg;
|
|
6
8
|
if (msg.silence_reason) return msg;
|
|
9
|
+
const legacy = msg as LegacyMessage;
|
|
10
|
+
const hasLegacy = 'verbal_response' in legacy || 'action_response' in legacy;
|
|
11
|
+
if (!hasLegacy) return msg;
|
|
7
12
|
const parts: string[] = [];
|
|
8
|
-
if (
|
|
9
|
-
if (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return { ...rest, content: parts.join('\n\n') };
|
|
13
|
+
if (legacy.action_response) parts.push(`_${legacy.action_response}_`);
|
|
14
|
+
if (legacy.verbal_response) parts.push(legacy.verbal_response);
|
|
15
|
+
const { verbal_response: _vr, action_response: _ar, ...rest } = legacy;
|
|
16
|
+
return parts.length > 0 ? { ...rest, content: parts.join('\n\n') } : rest as Message;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
export interface PersonaData {
|
|
@@ -47,7 +51,8 @@ export class PersonaState {
|
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
getByName(nameOrAlias: string): PersonaEntity | null {
|
|
50
|
-
|
|
54
|
+
// Same ZWS strip as resolveCanonicalAgent — see that function for the full explanation.
|
|
55
|
+
const searchTerm = nameOrAlias.replace(/^[\p{Z}\u200B\u200C\u200D\u2060\uFEFF]+|[\p{Z}\u200B\u200C\u200D\u2060\uFEFF]+$/gu, "").toLowerCase();
|
|
51
56
|
|
|
52
57
|
// Priority 1: Exact display_name match
|
|
53
58
|
for (const data of this.personas.values()) {
|
|
@@ -118,11 +123,18 @@ export class PersonaState {
|
|
|
118
123
|
return messages.map(m => ({ ...m }));
|
|
119
124
|
}
|
|
120
125
|
|
|
126
|
+
messages_getAlways(personaId: string): Message[] {
|
|
127
|
+
const messages = this.personas.get(personaId)?.messages ?? [];
|
|
128
|
+
return messages
|
|
129
|
+
.filter(m => m.context_status === "always")
|
|
130
|
+
.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
|
|
131
|
+
.map(m => ({ ...m }));
|
|
132
|
+
}
|
|
133
|
+
|
|
121
134
|
messages_append(personaId: string, message: Message): void {
|
|
122
135
|
const data = this.personas.get(personaId);
|
|
123
136
|
if (!data) return;
|
|
124
137
|
data.messages.push(message);
|
|
125
|
-
data.entity.last_activity = message.timestamp;
|
|
126
138
|
data.entity.last_updated = new Date().toISOString();
|
|
127
139
|
}
|
|
128
140
|
|
|
@@ -247,14 +259,11 @@ export class PersonaState {
|
|
|
247
259
|
return count;
|
|
248
260
|
}
|
|
249
261
|
|
|
250
|
-
messages_getUnextractedForPersona(personaId: string, shortId: string
|
|
262
|
+
messages_getUnextractedForPersona(personaId: string, shortId: string): Message[] {
|
|
251
263
|
const data = this.personas.get(personaId);
|
|
252
264
|
if (!data) return [];
|
|
253
265
|
return data.messages
|
|
254
|
-
.filter(m =>
|
|
255
|
-
if (sinceTimestamp && new Date(m.timestamp).getTime() < new Date(sinceTimestamp).getTime()) return false;
|
|
256
|
-
return !m.persona_extracted?.[shortId];
|
|
257
|
-
})
|
|
266
|
+
.filter(m => !m.persona_extracted?.[shortId])
|
|
258
267
|
.map(m => ({ ...m }));
|
|
259
268
|
}
|
|
260
269
|
|
package/src/core/state/rooms.ts
CHANGED
|
@@ -75,7 +75,6 @@ export class RoomState {
|
|
|
75
75
|
persona_ids: room.persona_ids,
|
|
76
76
|
active_node_id: room.active_node_id,
|
|
77
77
|
is_archived: room.is_archived,
|
|
78
|
-
last_activity: room.last_activity,
|
|
79
78
|
unread_count: this.messages_countUnread(roomId),
|
|
80
79
|
};
|
|
81
80
|
}
|
|
@@ -108,7 +107,6 @@ export class RoomState {
|
|
|
108
107
|
const room = this.rooms.get(roomId);
|
|
109
108
|
if (!room) return;
|
|
110
109
|
room.messages.push(message);
|
|
111
|
-
room.last_activity = message.timestamp;
|
|
112
110
|
room.last_updated = new Date().toISOString();
|
|
113
111
|
}
|
|
114
112
|
|
|
@@ -69,11 +69,48 @@ export class StateManager {
|
|
|
69
69
|
this.migrateMessageFlags();
|
|
70
70
|
this.migrateInterestedPersonas();
|
|
71
71
|
this.migrateProviderModel();
|
|
72
|
+
this.migratePersonaMessageContent();
|
|
72
73
|
this.migrateRoomMessageContent();
|
|
73
74
|
this.migrateThemes();
|
|
74
75
|
this.migrateFfaParentIds();
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
private migratePersonaMessageContent(): void {
|
|
79
|
+
// TODO(v1.0.0): Remove legacy persona message migration — verbal_response/action_response no longer written
|
|
80
|
+
const rawPersonas = (this.personaState as unknown as { personas: Map<string, { messages: Message[] }> }).personas;
|
|
81
|
+
let migratedCount = 0;
|
|
82
|
+
for (const [, data] of rawPersonas) {
|
|
83
|
+
const messages = data.messages;
|
|
84
|
+
for (const msg of messages) {
|
|
85
|
+
const legacy = msg as Message & { verbal_response?: string; action_response?: string };
|
|
86
|
+
if (!('verbal_response' in legacy || 'action_response' in legacy)) continue;
|
|
87
|
+
if (msg.content) {
|
|
88
|
+
delete (legacy as any).verbal_response;
|
|
89
|
+
delete (legacy as any).action_response;
|
|
90
|
+
migratedCount++;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (msg.silence_reason) {
|
|
94
|
+
delete (legacy as any).verbal_response;
|
|
95
|
+
delete (legacy as any).action_response;
|
|
96
|
+
migratedCount++;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const parts: string[] = [];
|
|
100
|
+
if (legacy.action_response) parts.push(`_${legacy.action_response}_`);
|
|
101
|
+
if (legacy.verbal_response) parts.push(legacy.verbal_response);
|
|
102
|
+
if (parts.length > 0) (msg as any).content = parts.join('\n\n');
|
|
103
|
+
delete (legacy as any).verbal_response;
|
|
104
|
+
delete (legacy as any).action_response;
|
|
105
|
+
migratedCount++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (migratedCount > 0) {
|
|
109
|
+
this.scheduleSave();
|
|
110
|
+
console.log(`[StateManager] Migrated ${migratedCount} persona messages to unified content field`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
77
114
|
private migrateRoomMessageContent(): void {
|
|
78
115
|
const rooms = this.roomState.getAll(true);
|
|
79
116
|
let migratedCount = 0;
|
|
@@ -81,14 +118,15 @@ export class StateManager {
|
|
|
81
118
|
for (const room of rooms) {
|
|
82
119
|
for (const msg of room.messages) {
|
|
83
120
|
if (msg.content) continue;
|
|
84
|
-
if (msg.role === 'human') continue;
|
|
85
121
|
if (msg.silence_reason) continue;
|
|
86
|
-
|
|
122
|
+
// TODO(v1.0.0): Remove legacy room message migration — verbal_response/action_response no longer written
|
|
87
123
|
const legacy = msg as RoomMessage & { verbal_response?: string; action_response?: string };
|
|
124
|
+
const hasLegacy = 'verbal_response' in legacy || 'action_response' in legacy;
|
|
125
|
+
if (!hasLegacy) continue;
|
|
126
|
+
const parts: string[] = [];
|
|
88
127
|
if (legacy.action_response) parts.push(`_${legacy.action_response}_`);
|
|
89
128
|
if (legacy.verbal_response) parts.push(legacy.verbal_response);
|
|
90
|
-
if (parts.length
|
|
91
|
-
msg.content = parts.join('\n\n');
|
|
129
|
+
if (parts.length > 0) msg.content = parts.join('\n\n');
|
|
92
130
|
delete (msg as any).verbal_response;
|
|
93
131
|
delete (msg as any).action_response;
|
|
94
132
|
migratedCount++;
|
|
@@ -452,8 +490,14 @@ export class StateManager {
|
|
|
452
490
|
|
|
453
491
|
getRoomList(includeArchived = false): RoomSummary[] {
|
|
454
492
|
return this.roomState.getAll(includeArchived)
|
|
455
|
-
.
|
|
456
|
-
|
|
493
|
+
.sort((a, b) => {
|
|
494
|
+
const lastMsg = (room: typeof a) => {
|
|
495
|
+
const msgs = room.messages;
|
|
496
|
+
return msgs.length > 0 ? new Date(msgs[msgs.length - 1].timestamp).getTime() : 0;
|
|
497
|
+
};
|
|
498
|
+
return lastMsg(b) - lastMsg(a);
|
|
499
|
+
})
|
|
500
|
+
.map(r => this.roomState.getSummary(r.id)!);
|
|
457
501
|
}
|
|
458
502
|
|
|
459
503
|
getRoom(roomId: string): RoomEntity | null {
|
|
@@ -470,7 +514,7 @@ export class StateManager {
|
|
|
470
514
|
id: crypto.randomUUID(),
|
|
471
515
|
parent_id: null,
|
|
472
516
|
role: "human",
|
|
473
|
-
|
|
517
|
+
content: input.initial_message,
|
|
474
518
|
timestamp: now,
|
|
475
519
|
read: true,
|
|
476
520
|
context_status: "default" as import("./types.js").ContextStatus,
|
|
@@ -486,7 +530,6 @@ export class StateManager {
|
|
|
486
530
|
is_archived: false,
|
|
487
531
|
created_at: now,
|
|
488
532
|
last_updated: now,
|
|
489
|
-
last_activity: now,
|
|
490
533
|
messages: [initialMessage],
|
|
491
534
|
};
|
|
492
535
|
this.roomState.add(room);
|
|
@@ -691,8 +734,9 @@ export class StateManager {
|
|
|
691
734
|
}
|
|
692
735
|
|
|
693
736
|
human_person_getByIdentifier(type: string | null, value: string): Person | undefined {
|
|
737
|
+
const typeLower = type?.toLowerCase();
|
|
694
738
|
return this.getHuman().people.find(p =>
|
|
695
|
-
p.identifiers?.some(i => (!
|
|
739
|
+
p.identifiers?.some(i => (!typeLower || i.type.toLowerCase() === typeLower) && i.value === value)
|
|
696
740
|
);
|
|
697
741
|
}
|
|
698
742
|
|
|
@@ -773,6 +817,34 @@ export class StateManager {
|
|
|
773
817
|
return this.personaState.messages_get(personaId);
|
|
774
818
|
}
|
|
775
819
|
|
|
820
|
+
messages_getAlways(personaId: string): Message[] {
|
|
821
|
+
return this.personaState.messages_getAlways(personaId);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Returns the timestamp (epoch ms) of the last internal Ei message for a persona.
|
|
826
|
+
* Always excludes external=true messages (imported from OpenCode, Cursor, Claude Code).
|
|
827
|
+
*
|
|
828
|
+
* @param personaId - The persona to query
|
|
829
|
+
* @param mode - Optional filter:
|
|
830
|
+
* omitted → latest message in either direction (human or persona)
|
|
831
|
+
* 'self' → latest message from the persona (role='system'), silence counts
|
|
832
|
+
* 'human' → latest message from the human (role='human')
|
|
833
|
+
* @returns epoch ms, or 0 if no matching messages found
|
|
834
|
+
*/
|
|
835
|
+
messages_getLastActivity(personaId: string, mode?: 'self' | 'human'): number {
|
|
836
|
+
const messages = this.personaState.messages_get(personaId);
|
|
837
|
+
const filtered = messages.filter(m => {
|
|
838
|
+
if (m.external === true) return false;
|
|
839
|
+
if (mode === 'self') return m.role === 'system';
|
|
840
|
+
if (mode === 'human') return m.role === 'human';
|
|
841
|
+
return true;
|
|
842
|
+
});
|
|
843
|
+
if (filtered.length === 0) return 0;
|
|
844
|
+
const last = filtered[filtered.length - 1];
|
|
845
|
+
return new Date(last.timestamp).getTime();
|
|
846
|
+
}
|
|
847
|
+
|
|
776
848
|
messages_append(personaId: string, message: Message): void {
|
|
777
849
|
this.personaState.messages_append(personaId, message);
|
|
778
850
|
this.scheduleSave();
|
|
@@ -837,8 +909,8 @@ export class StateManager {
|
|
|
837
909
|
return result;
|
|
838
910
|
}
|
|
839
911
|
|
|
840
|
-
messages_getUnextractedForPersona(personaId: string, shortId: string
|
|
841
|
-
return this.personaState.messages_getUnextractedForPersona(personaId, shortId
|
|
912
|
+
messages_getUnextractedForPersona(personaId: string, shortId: string): Message[] {
|
|
913
|
+
return this.personaState.messages_getUnextractedForPersona(personaId, shortId);
|
|
842
914
|
}
|
|
843
915
|
|
|
844
916
|
messages_markPersonaExtracted(personaId: string, messageIds: string[], shortId: string): number {
|
|
@@ -57,8 +57,8 @@ export interface PersonaTopic {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
export interface PersonIdentifier {
|
|
60
|
-
type: string; // User-extensible. "
|
|
61
|
-
value: string; // The identifier value. For
|
|
60
|
+
type: string; // User-extensible. "Nickname", "GitHub", "Ei Persona", etc. NOT an enum. Matching is case-insensitive.
|
|
61
|
+
value: string; // The identifier value. For "Ei Persona": the Persona UUID.
|
|
62
62
|
is_primary?: boolean; // True = this is the display name. Synced to DataItemBase.name on write.
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -107,7 +107,6 @@ export interface HumanSettings {
|
|
|
107
107
|
queue_paused?: boolean;
|
|
108
108
|
skip_quote_delete_confirm?: boolean;
|
|
109
109
|
name_display?: string;
|
|
110
|
-
time_mode?: "24h" | "12h" | "local" | "utc";
|
|
111
110
|
default_heartbeat_ms?: number;
|
|
112
111
|
default_context_window_hours?: number;
|
|
113
112
|
message_min_count?: number;
|
|
@@ -130,7 +129,6 @@ export interface HumanEntity {
|
|
|
130
129
|
people: Person[];
|
|
131
130
|
quotes: Quote[];
|
|
132
131
|
last_updated: string;
|
|
133
|
-
last_activity: string;
|
|
134
132
|
settings?: HumanSettings;
|
|
135
133
|
}
|
|
136
134
|
|
|
@@ -156,12 +154,19 @@ export interface PersonaEntity {
|
|
|
156
154
|
include_message_timestamps?: boolean; // Prepend ISO timestamp to each message sent to the LLM
|
|
157
155
|
context_boundary?: string; // ISO timestamp - messages before this excluded from LLM context
|
|
158
156
|
last_updated: string;
|
|
159
|
-
last_activity: string;
|
|
160
157
|
last_heartbeat?: string;
|
|
161
158
|
last_extraction?: string;
|
|
162
159
|
tools?: string[]; // IDs of ToolDefinitions this persona can use. Empty/absent = no tool access.
|
|
163
160
|
reflection_last_asked?: string; // ISO timestamp. Set ONLY when Persona explicitly surfaces identity drift (mentioned_reflection: true).
|
|
164
161
|
description_embedding?: number[]; // Embedding of long_description (short_description fallback). Excludes traits. See embedding-service.ts:getPersonaDescriptionText.
|
|
162
|
+
pending_update?: { // Proposed identity revision from ceremony reflection. Cleared when user accepts or dismisses via /reflect apply.
|
|
163
|
+
short_description: string;
|
|
164
|
+
long_description: string;
|
|
165
|
+
traits: import("./data-items.js").PersonaTrait[];
|
|
166
|
+
topics: import("./data-items.js").PersonaTopic[];
|
|
167
|
+
critique: string; // Analyst's prose — for Ei to use when urgency is high, not surfaced to the persona.
|
|
168
|
+
created_at: string; // ISO timestamp of when the Critic ran. Used to name the reflect folder.
|
|
169
|
+
};
|
|
165
170
|
avatar_emoji?: string; // Single emoji character used as avatar in place of initials.
|
|
166
171
|
avatar_image?: string; // Base64-encoded 64×64 image used as avatar (takes priority over avatar_emoji).
|
|
167
172
|
preferred_theme?: string; // Theme ID (built-in name or ThemeDefinition.id). Applied to chat panel when this persona is active.
|
package/src/core/types/enums.ts
CHANGED
|
@@ -53,6 +53,7 @@ export enum LLMNextStep {
|
|
|
53
53
|
HandlePersonaPreview = "handlePersonaPreview",
|
|
54
54
|
HandlePersonIdentifierMigration = "handlePersonIdentifierMigration",
|
|
55
55
|
HandleTopicValidate = "handleTopicValidate",
|
|
56
|
+
HandleReflectionCritic = "handleReflectionCritic",
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
export enum ProviderType {
|
|
@@ -54,11 +54,11 @@ export interface PersonaSummary {
|
|
|
54
54
|
is_paused: boolean;
|
|
55
55
|
is_archived: boolean;
|
|
56
56
|
unread_count: number;
|
|
57
|
-
last_activity?: string;
|
|
58
57
|
context_boundary?: string;
|
|
59
58
|
avatar_emoji?: string;
|
|
60
59
|
avatar_image?: string;
|
|
61
60
|
preferred_theme?: string;
|
|
61
|
+
has_pending_update: boolean;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export interface MessageQueryOptions {
|
package/src/core/types/llm.ts
CHANGED
|
@@ -8,10 +8,8 @@ import type { LLMRequestType, LLMPriority, LLMNextStep } from "./enums.js";
|
|
|
8
8
|
export interface Message {
|
|
9
9
|
id: string;
|
|
10
10
|
role: "human" | "system";
|
|
11
|
-
content?: string; // Raw Markdown response
|
|
12
|
-
|
|
13
|
-
action_response?: string; // Stage direction / action the persona performs
|
|
14
|
-
silence_reason?: string; // Why the persona chose not to respond (not shown to LLM)
|
|
11
|
+
content?: string; // Raw Markdown response
|
|
12
|
+
silence_reason?: string; // Why the sender chose not to respond (human or persona)
|
|
15
13
|
timestamp: string;
|
|
16
14
|
read: boolean; // Has human seen this system message?
|
|
17
15
|
context_status: import("./enums.js").ContextStatus;
|
package/src/core/types/rooms.ts
CHANGED
|
@@ -11,8 +11,6 @@ export interface RoomMessage {
|
|
|
11
11
|
role: "human" | "persona";
|
|
12
12
|
persona_id?: string;
|
|
13
13
|
content?: string;
|
|
14
|
-
verbal_response?: string;
|
|
15
|
-
action_response?: string;
|
|
16
14
|
silence_reason?: string;
|
|
17
15
|
timestamp: string;
|
|
18
16
|
read: boolean;
|
|
@@ -36,7 +34,6 @@ export interface RoomEntity {
|
|
|
36
34
|
is_archived: boolean;
|
|
37
35
|
created_at: string;
|
|
38
36
|
last_updated: string;
|
|
39
|
-
last_activity: string;
|
|
40
37
|
capture_used?: boolean;
|
|
41
38
|
context_window_hours?: number; // FFA only; falls back to human.settings.default_context_window_hours
|
|
42
39
|
context_boundary?: string; // FFA only; ISO timestamp; same semantics as persona context_boundary
|
|
@@ -58,6 +55,5 @@ export interface RoomSummary {
|
|
|
58
55
|
persona_ids: string[];
|
|
59
56
|
active_node_id: string | null;
|
|
60
57
|
is_archived: boolean;
|
|
61
|
-
last_activity: string;
|
|
62
58
|
unread_count: number;
|
|
63
59
|
}
|
|
@@ -43,7 +43,7 @@ function convertToEiMessage(msg: ClaudeCodeMessage): Message {
|
|
|
43
43
|
return {
|
|
44
44
|
id: msg.id,
|
|
45
45
|
role: msg.role === "user" ? "human" : "system",
|
|
46
|
-
|
|
46
|
+
content: msg.content,
|
|
47
47
|
timestamp: msg.timestamp,
|
|
48
48
|
read: true,
|
|
49
49
|
context_status: "default" as ContextStatus,
|
|
@@ -99,7 +99,6 @@ function ensureClaudeCodePersona(
|
|
|
99
99
|
heartbeat_delay_ms: TWELVE_HOURS_MS,
|
|
100
100
|
last_heartbeat: now,
|
|
101
101
|
last_updated: now,
|
|
102
|
-
last_activity: now,
|
|
103
102
|
};
|
|
104
103
|
|
|
105
104
|
stateManager.persona_add(persona);
|
|
@@ -252,9 +251,6 @@ export async function importClaudeCodeSessions(
|
|
|
252
251
|
}
|
|
253
252
|
|
|
254
253
|
stateManager.messages_sort(persona.id);
|
|
255
|
-
stateManager.persona_update(persona.id, {
|
|
256
|
-
last_activity: new Date().toISOString(),
|
|
257
|
-
});
|
|
258
254
|
eiInterface?.onMessageAdded?.(persona.id);
|
|
259
255
|
|
|
260
256
|
// ─── Step 5: Queue extraction for new messages ────────────────────────
|
|
@@ -35,7 +35,7 @@ function convertToEiMessage(msg: CursorMessage): Message {
|
|
|
35
35
|
return {
|
|
36
36
|
id: msg.id,
|
|
37
37
|
role: msg.type === 1 ? "human" : "system",
|
|
38
|
-
|
|
38
|
+
content: msg.text,
|
|
39
39
|
timestamp: msg.timestamp,
|
|
40
40
|
read: true,
|
|
41
41
|
context_status: "default" as ContextStatus,
|
|
@@ -87,7 +87,6 @@ function ensureCursorPersona(
|
|
|
87
87
|
heartbeat_delay_ms: TWELVE_HOURS_MS,
|
|
88
88
|
last_heartbeat: now,
|
|
89
89
|
last_updated: now,
|
|
90
|
-
last_activity: now,
|
|
91
90
|
};
|
|
92
91
|
|
|
93
92
|
stateManager.persona_add(persona);
|
|
@@ -212,9 +211,6 @@ export async function importCursorSessions(
|
|
|
212
211
|
}
|
|
213
212
|
|
|
214
213
|
stateManager.messages_sort(persona.id);
|
|
215
|
-
stateManager.persona_update(persona.id, {
|
|
216
|
-
last_activity: new Date().toISOString(),
|
|
217
|
-
});
|
|
218
214
|
eiInterface?.onMessageAdded?.(persona.id);
|
|
219
215
|
|
|
220
216
|
if (toAnalyze.length > 0 && !signal?.aborted) {
|
|
@@ -47,7 +47,7 @@ function convertToEiMessage(ocMsg: OpenCodeMessage): Message {
|
|
|
47
47
|
return {
|
|
48
48
|
id: ocMsg.id,
|
|
49
49
|
role: ocMsg.role === "user" ? "human" : "system",
|
|
50
|
-
|
|
50
|
+
content: ocMsg.content,
|
|
51
51
|
timestamp: ocMsg.timestamp,
|
|
52
52
|
read: true,
|
|
53
53
|
context_status: "default" as ContextStatus,
|
|
@@ -234,9 +234,6 @@ export async function importOpenCodeSessions(
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
stateManager.messages_sort(persona.id);
|
|
237
|
-
stateManager.persona_update(persona.id, {
|
|
238
|
-
last_activity: new Date().toISOString(),
|
|
239
|
-
});
|
|
240
237
|
eiInterface?.onMessageAdded?.(persona.id);
|
|
241
238
|
|
|
242
239
|
// ─── Step 5: Queue extraction for unmarked messages ────────────────
|
|
@@ -176,10 +176,19 @@ export const AGENT_TO_AGENT_PREFIXES = [
|
|
|
176
176
|
*/
|
|
177
177
|
export const AGENT_ALIASES: Record<string, string[]> = {
|
|
178
178
|
// ── OhMyOpenCode primary agents ──────────────────────────────────────────
|
|
179
|
+
//
|
|
180
|
+
// oh-my-openagent uses "Foo - Bar" display names for its agents and stores
|
|
181
|
+
// them verbatim in OpenCode's SQLite message rows. It also prefixes them
|
|
182
|
+
// with invisible U+200B ZERO WIDTH SPACE characters as a sort hack (1 ZWS
|
|
183
|
+
// for Sisyphus, 2 for Hephaestus, etc.) — those are stripped upstream in
|
|
184
|
+
// resolveCanonicalAgent before the alias lookup, so only the clean form is
|
|
185
|
+
// needed here. Parenthetical variants ("Foo (Bar)") are legacy names from
|
|
186
|
+
// earlier oh-my-openagent versions, kept for backward compatibility.
|
|
179
187
|
Sisyphus: [
|
|
180
188
|
"sisyphus",
|
|
181
189
|
"Sisyphus",
|
|
182
|
-
"Sisyphus
|
|
190
|
+
"Sisyphus - Ultraworker", // oh-my-openagent ≥ 3.x display name (stored in OpenCode DB)
|
|
191
|
+
"Sisyphus (Ultraworker)", // legacy oh-my-openagent display name
|
|
183
192
|
"Sisyphus Ultraworker",
|
|
184
193
|
"sisyphus ultraworker",
|
|
185
194
|
"Planner-Sisyphus",
|
|
@@ -190,21 +199,28 @@ export const AGENT_ALIASES: Record<string, string[]> = {
|
|
|
190
199
|
Atlas: [
|
|
191
200
|
"atlas",
|
|
192
201
|
"Atlas",
|
|
202
|
+
"Atlas - Plan Executor", // oh-my-openagent ≥ 3.x display name
|
|
203
|
+
"Atlas (Plan Executor)", // mixed-case variant observed in DB
|
|
193
204
|
"atlas (plan executor)",
|
|
194
205
|
"Atlas (plan executor)",
|
|
195
206
|
],
|
|
196
207
|
Prometheus: [
|
|
197
208
|
"prometheus",
|
|
198
209
|
"Prometheus",
|
|
210
|
+
"Prometheus - Plan Builder", // oh-my-openagent ≥ 3.x display name
|
|
199
211
|
"prometheus (plan builder)",
|
|
200
212
|
"Prometheus (plan builder)",
|
|
201
213
|
],
|
|
202
214
|
Hephaestus: [
|
|
203
215
|
"hephaestus",
|
|
204
216
|
"Hephaestus",
|
|
217
|
+
"Hephaestus - Deep Agent", // oh-my-openagent ≥ 3.x display name
|
|
205
218
|
"hephaestus (deep agent)",
|
|
206
219
|
"Hephaestus (deep agent)",
|
|
207
220
|
],
|
|
221
|
+
// Metis, Momus, Sisyphus-Junior intentionally absent — they are subagent-only.
|
|
222
|
+
// The sqlite reader filters to parent_id IS NULL (primary sessions only), so
|
|
223
|
+
// messages from these agents can never reach the importer.
|
|
208
224
|
|
|
209
225
|
// ── ai-sdlc agents (RobotsAndPencils/ai-sdlc-claude-code-template) ───────
|
|
210
226
|
// Installed via scripts/install-opencode.sh as "ai-sdlc-{name}" in OpenCode.
|
|
@@ -33,8 +33,8 @@ function formatPeopleWithGaps(people: Person[]): string {
|
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* A "real" persona message is one the persona actually said to the human.
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
* silence_reason messages (sender chose not to speak) and empty messages
|
|
37
|
+
* don't count as conversational outreach.
|
|
38
38
|
*/
|
|
39
39
|
function isConversationalMessage(m: Message): boolean {
|
|
40
40
|
if (m.role !== 'system') return false;
|
|
@@ -124,20 +124,10 @@ ${formatPeopleWithGaps(data.human.people)}`;
|
|
|
124
124
|
|
|
125
125
|
**Quality over quantity** - Only reach out if you have something real to say.`;
|
|
126
126
|
|
|
127
|
-
const
|
|
128
|
-
? `## Identity
|
|
127
|
+
const pendingUpdateFragment = data.persona.has_pending_update
|
|
128
|
+
? `## Pending Identity Changes
|
|
129
129
|
|
|
130
|
-
The
|
|
131
|
-
|
|
132
|
-
**How the human currently describes you:**
|
|
133
|
-
${data.drift_context.people_description}
|
|
134
|
-
|
|
135
|
-
**Your current definition:**
|
|
136
|
-
${data.drift_context.persona_description}
|
|
137
|
-
|
|
138
|
-
If you feel these describe meaningfully different versions of you, you may want to gently surface this — in your own way, in your own time. If they seem the same to you, no need to mention it.
|
|
139
|
-
|
|
140
|
-
If you do choose to address this, include \`"mentioned_reflection": true\` in your response.`
|
|
130
|
+
Based on recent conversations, there are proposed updates to your identity waiting for review. The user has been notified and can review them. You may bring this up if it feels right — or not. It's yours to decide.`
|
|
141
131
|
: '';
|
|
142
132
|
|
|
143
133
|
const outputFragment = `## Response Format
|
|
@@ -148,8 +138,7 @@ Call the \`submit_heartbeat_check\` tool with your decision. If the tool is unav
|
|
|
148
138
|
{
|
|
149
139
|
"should_respond": true,
|
|
150
140
|
"topic": "the specific topic you want to discuss",
|
|
151
|
-
"message": "Your actual message to them (if should_respond is true)"
|
|
152
|
-
"mentioned_reflection": true` : ''}
|
|
141
|
+
"message": "Your actual message to them (if should_respond is true)"
|
|
153
142
|
}
|
|
154
143
|
\`\`\`
|
|
155
144
|
|
|
@@ -165,7 +154,7 @@ If you decide NOT to reach out:
|
|
|
165
154
|
contextFragment,
|
|
166
155
|
opportunitiesFragment,
|
|
167
156
|
guidelinesFragment,
|
|
168
|
-
|
|
157
|
+
pendingUpdateFragment,
|
|
169
158
|
outputFragment,
|
|
170
159
|
].filter(Boolean).join('\n\n');
|
|
171
160
|
|
|
@@ -28,6 +28,19 @@ function formatItem(item: EiHeartbeatItem): string {
|
|
|
28
28
|
const desc = item.short_description ? ` — ${item.short_description}` : "";
|
|
29
29
|
return `- **${item.id}** Inactive Persona: ${item.name}${desc} (${item.days_inactive} days inactive)`;
|
|
30
30
|
}
|
|
31
|
+
case "New Person":
|
|
32
|
+
return [
|
|
33
|
+
`- **${item.id}** New Person: ${item.name}`,
|
|
34
|
+
` ${item.description}`,
|
|
35
|
+
item.quote ? ` Quote: "${item.quote}"` : "",
|
|
36
|
+
].filter(Boolean).join("\n");
|
|
37
|
+
|
|
38
|
+
case "Persona Reflection Alert":
|
|
39
|
+
return [
|
|
40
|
+
`- **${item.id}** Persona Reflection Alert: ${item.persona_name}`,
|
|
41
|
+
` ${item.critique}`,
|
|
42
|
+
].join("\n");
|
|
43
|
+
|
|
31
44
|
default:
|
|
32
45
|
return '';
|
|
33
46
|
}
|
|
@@ -72,6 +85,7 @@ ${itemsSection}
|
|
|
72
85
|
- **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.
|
|
73
86
|
- **Low-Engagement Person / Topic**: Write a natural, warm message that naturally brings up this person or topic. Set the id and my_response.
|
|
74
87
|
- **Inactive Persona**: Write a message that gently mentions the persona might be worth checking in with. Set the id and my_response.
|
|
88
|
+
- **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.
|
|
75
89
|
|
|
76
90
|
## When NOT to Reach Out
|
|
77
91
|
|
|
@@ -21,6 +21,7 @@ export interface HeartbeatCheckPromptData {
|
|
|
21
21
|
name: string;
|
|
22
22
|
traits: PersonaTrait[];
|
|
23
23
|
topics: PersonaTopic[];
|
|
24
|
+
has_pending_update: boolean;
|
|
24
25
|
};
|
|
25
26
|
human: {
|
|
26
27
|
topics: Topic[]; // Filtered, sorted by engagement gap
|
|
@@ -28,10 +29,6 @@ export interface HeartbeatCheckPromptData {
|
|
|
28
29
|
};
|
|
29
30
|
recent_history: Message[]; // Last N messages for context
|
|
30
31
|
inactive_days: number; // Days since last activity
|
|
31
|
-
drift_context?: {
|
|
32
|
-
people_description: string;
|
|
33
|
-
persona_description: string;
|
|
34
|
-
};
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
/**
|
|
@@ -41,7 +38,6 @@ export interface HeartbeatCheckResult {
|
|
|
41
38
|
should_respond: boolean;
|
|
42
39
|
topic?: string;
|
|
43
40
|
message?: string;
|
|
44
|
-
mentioned_reflection?: boolean;
|
|
45
41
|
}
|
|
46
42
|
|
|
47
43
|
// =============================================================================
|
|
@@ -91,6 +87,12 @@ export type EiHeartbeatItem =
|
|
|
91
87
|
name: string;
|
|
92
88
|
description: string;
|
|
93
89
|
quote?: string;
|
|
90
|
+
}
|
|
91
|
+
| {
|
|
92
|
+
id: string;
|
|
93
|
+
type: "Persona Reflection Alert";
|
|
94
|
+
persona_name: string;
|
|
95
|
+
critique: string;
|
|
94
96
|
};
|
|
95
97
|
|
|
96
98
|
/**
|
package/src/prompts/index.ts
CHANGED
|
@@ -81,6 +81,15 @@ export {
|
|
|
81
81
|
buildRoomResponsePrompt,
|
|
82
82
|
buildRoomJudgePrompt,
|
|
83
83
|
} from "./room/index.js";
|
|
84
|
+
|
|
85
|
+
export { buildReflectionCriticPrompt } from "./reflection/index.js";
|
|
86
|
+
export type {
|
|
87
|
+
ReflectionCriticPromptData,
|
|
88
|
+
ReflectionCriticResult,
|
|
89
|
+
PersonaIdentitySnapshot,
|
|
90
|
+
} from "./reflection/types.js";
|
|
91
|
+
|
|
92
|
+
|
|
84
93
|
export type {
|
|
85
94
|
RoomResponsePromptData,
|
|
86
95
|
RoomJudgePromptData,
|
|
@@ -8,24 +8,27 @@ export function getMessageDisplayText(message: Message): string | null {
|
|
|
8
8
|
const content = getMessageContent(message);
|
|
9
9
|
if (content) parts.push(content);
|
|
10
10
|
if (message.silence_reason) {
|
|
11
|
-
const name = message.
|
|
11
|
+
const name = message.role === "human"
|
|
12
|
+
? (message.speaker_name ?? 'Human')
|
|
13
|
+
: (message.speaker_name ?? 'Persona');
|
|
12
14
|
parts.push(`[${name} chose not to respond because: ${message.silence_reason}]`);
|
|
13
15
|
}
|
|
14
16
|
if (parts.length === 0) return null;
|
|
15
17
|
return parts.join('\n\n');
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
export function buildChatMessageContent(message: Message): string {
|
|
20
|
+
export function buildChatMessageContent(message: Message, humanName?: string): string {
|
|
19
21
|
const parts: string[] = [];
|
|
20
22
|
const content = getMessageContent(message);
|
|
21
23
|
|
|
22
24
|
if (message._synthesis && content) {
|
|
23
|
-
parts.push(`[The user used your conversation to generate an image. The full prompt was: "${content}"]`);
|
|
25
|
+
parts.push(`[${humanName ?? 'The user'} used your conversation to generate an image. The full prompt was: "${content}"]`);
|
|
24
26
|
} else if (content) {
|
|
25
27
|
parts.push(content);
|
|
26
28
|
}
|
|
27
29
|
if (message.silence_reason) {
|
|
28
|
-
|
|
30
|
+
const silentParty = message.role === "human" ? (humanName ?? "The human") : "You";
|
|
31
|
+
parts.push(`${silentParty} chose not to respond because: ${message.silence_reason}`);
|
|
29
32
|
}
|
|
30
33
|
return parts.join('\n\n');
|
|
31
34
|
}
|