ei-tui 1.1.0 → 1.3.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 +16 -0
- package/package.json +2 -23
- package/src/cli/README.md +12 -2
- package/src/cli/mcp.ts +12 -4
- package/src/cli/retrieval.ts +162 -0
- package/src/cli.ts +7 -1
- package/src/core/handlers/dedup.ts +4 -15
- package/src/core/handlers/document-segmentation.ts +5 -7
- package/src/core/handlers/heartbeat.ts +5 -10
- package/src/core/handlers/human-matching.ts +8 -0
- package/src/core/handlers/index.ts +2 -0
- package/src/core/handlers/knowledge-synthesis.ts +48 -0
- package/src/core/handlers/persona-generation.ts +4 -8
- package/src/core/handlers/persona-response.ts +3 -4
- package/src/core/handlers/persona-topics.ts +2 -4
- package/src/core/handlers/rewrite.ts +26 -9
- package/src/core/handlers/rooms.ts +6 -12
- package/src/core/heartbeat-manager.ts +10 -0
- package/src/core/llm-client.ts +13 -3
- package/src/core/message-manager.ts +2 -4
- package/src/core/orchestrators/ceremony.ts +45 -22
- package/src/core/orchestrators/human-extraction.ts +10 -1
- package/src/core/processor.ts +275 -7
- package/src/core/queue-manager.ts +10 -0
- package/src/core/state-manager.ts +35 -0
- package/src/core/tools/builtin/fetch-memory.ts +6 -6
- package/src/core/tools/builtin/fetch-message.ts +27 -1
- package/src/core/tools/builtin/find-memory.ts +11 -3
- package/src/core/tools/index.ts +3 -3
- package/src/core/tools/types.ts +1 -1
- package/src/core/types/data-items.ts +1 -1
- package/src/core/types/entities.ts +7 -1
- package/src/core/types/enums.ts +1 -0
- package/src/core/types/integrations.ts +3 -1
- package/src/core/types/llm.ts +0 -9
- package/src/core/utils/message-id.ts +114 -0
- package/src/integrations/claude-code/importer.ts +12 -5
- package/src/integrations/cursor/importer.ts +12 -5
- package/src/integrations/document/importer.ts +1 -1
- package/src/integrations/document/unsource.ts +11 -14
- package/src/integrations/opencode/importer.ts +19 -6
- package/src/integrations/opencode/json-reader.ts +65 -0
- package/src/integrations/opencode/sqlite-reader.ts +33 -0
- package/src/integrations/opencode/types.ts +8 -0
- package/src/integrations/persona-history/importer.ts +9 -0
- package/src/prompts/ceremony/people-rewrite.ts +2 -2
- package/src/prompts/ceremony/topic-rewrite.ts +2 -2
- package/src/prompts/heartbeat/check.ts +5 -2
- package/src/prompts/heartbeat/ei.ts +7 -0
- package/src/prompts/heartbeat/types.ts +5 -0
- package/src/prompts/index.ts +3 -0
- package/src/prompts/response/sections.ts +30 -16
- package/src/prompts/room/sections.ts +28 -6
- package/src/prompts/synthesis/index.ts +101 -0
- package/src/prompts/synthesis/types.ts +26 -0
- package/src/prompts/trait-utils.ts +33 -0
- package/tui/README.md +2 -0
- package/tui/src/commands/generate.tsx +98 -0
- package/tui/src/commands/unsource.tsx +17 -10
- package/tui/src/components/GeneratedDocsOverlay.tsx +136 -0
- package/tui/src/components/PromptInput.tsx +2 -0
- package/tui/src/context/ei.tsx +49 -2
- package/tui/src/util/help-content.ts +11 -0
- package/tui/src/util/logger.ts +22 -2
- package/tui/src/util/provider-detection.ts +5 -2
- package/tui/src/util/yaml-provider.ts +2 -8
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fully-qualified message ID format:
|
|
3
|
+
* ei:${uuid}
|
|
4
|
+
* opencode:${machine}:${session}:${nativeId}
|
|
5
|
+
* claudecode:${machine}:${session}:${nativeId}
|
|
6
|
+
* cursor:${machine}:${session}:${nativeId}
|
|
7
|
+
* import:document:${slug}:${uuid}
|
|
8
|
+
* slack:${workspace}:${channel}:${ts}
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type MessageIdIntegration =
|
|
12
|
+
| "ei"
|
|
13
|
+
| "opencode"
|
|
14
|
+
| "claudecode"
|
|
15
|
+
| "cursor"
|
|
16
|
+
| "import"
|
|
17
|
+
| "slack"
|
|
18
|
+
| "unknown"
|
|
19
|
+
|
|
20
|
+
export interface ParsedMessageId {
|
|
21
|
+
integration: MessageIdIntegration
|
|
22
|
+
machine?: string
|
|
23
|
+
session?: string
|
|
24
|
+
nativeId: string
|
|
25
|
+
raw: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseMessageId(id: string): ParsedMessageId {
|
|
29
|
+
if (!id || typeof id !== "string") {
|
|
30
|
+
const raw = String(id ?? "")
|
|
31
|
+
return { integration: "unknown", nativeId: raw, raw }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const parts = id.split(":")
|
|
35
|
+
|
|
36
|
+
if (parts[0] === "ei" && parts.length === 2) {
|
|
37
|
+
return { integration: "ei", nativeId: parts[1], raw: id }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (parts[0] === "opencode" && parts.length >= 4) {
|
|
41
|
+
return {
|
|
42
|
+
integration: "opencode",
|
|
43
|
+
machine: parts[1],
|
|
44
|
+
session: parts[2],
|
|
45
|
+
nativeId: parts.slice(3).join(":"),
|
|
46
|
+
raw: id,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (parts[0] === "claudecode" && parts.length >= 4) {
|
|
51
|
+
return {
|
|
52
|
+
integration: "claudecode",
|
|
53
|
+
machine: parts[1],
|
|
54
|
+
session: parts[2],
|
|
55
|
+
nativeId: parts.slice(3).join(":"),
|
|
56
|
+
raw: id,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (parts[0] === "cursor" && parts.length >= 4) {
|
|
61
|
+
return {
|
|
62
|
+
integration: "cursor",
|
|
63
|
+
machine: parts[1],
|
|
64
|
+
session: parts[2],
|
|
65
|
+
nativeId: parts.slice(3).join(":"),
|
|
66
|
+
raw: id,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (parts[0] === "import" && parts[1] === "document" && parts.length >= 4) {
|
|
71
|
+
return {
|
|
72
|
+
integration: "import",
|
|
73
|
+
session: parts[2],
|
|
74
|
+
nativeId: parts.slice(3).join(":"),
|
|
75
|
+
raw: id,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (parts[0] === "slack" && parts.length >= 4) {
|
|
80
|
+
return {
|
|
81
|
+
integration: "slack",
|
|
82
|
+
machine: parts[1],
|
|
83
|
+
session: parts[2],
|
|
84
|
+
nativeId: parts.slice(3).join(":"),
|
|
85
|
+
raw: id,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { integration: "unknown", nativeId: id, raw: id }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function isQualifiedMessageId(id: string): boolean {
|
|
93
|
+
return id.includes(":")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function qualifyEiMessage(uuid: string): string {
|
|
97
|
+
return `ei:${uuid}`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function qualifyOpenCodeMessage(machine: string, sessionId: string, nativeId: string): string {
|
|
101
|
+
return `opencode:${machine}:${sessionId}:${nativeId}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function qualifyClaudeCodeMessage(machine: string, sessionId: string, nativeId: string): string {
|
|
105
|
+
return `claudecode:${machine}:${sessionId}:${nativeId}`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function qualifyCursorMessage(machine: string, sessionId: string, nativeId: string): string {
|
|
109
|
+
return `cursor:${machine}:${sessionId}:${nativeId}`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function qualifyDocumentMessage(slug: string, uuid: string): string {
|
|
113
|
+
return `import:document:${slug}:${uuid}`
|
|
114
|
+
}
|
|
@@ -11,8 +11,13 @@ import {
|
|
|
11
11
|
queueAllScans,
|
|
12
12
|
type ExtractionContext,
|
|
13
13
|
} from "../../core/orchestrators/human-extraction.js";
|
|
14
|
+
import {
|
|
15
|
+
queuePersonRewritePhase,
|
|
16
|
+
queueTopicRewritePhase,
|
|
17
|
+
} from "../../core/orchestrators/ceremony.js";
|
|
14
18
|
import { isProcessRunning } from "../process-check.js";
|
|
15
19
|
import { getMachineId } from "../machine-id.js";
|
|
20
|
+
import { qualifyClaudeCodeMessage } from "../../core/utils/message-id.js";
|
|
16
21
|
|
|
17
22
|
// =============================================================================
|
|
18
23
|
// Export Types
|
|
@@ -39,9 +44,9 @@ export interface ClaudeCodeImporterOptions {
|
|
|
39
44
|
const TWELVE_HOURS_MS = 43_200_000;
|
|
40
45
|
const CLAUDE_CODE_GROUP = "Claude Code";
|
|
41
46
|
|
|
42
|
-
function convertToEiMessage(msg: ClaudeCodeMessage): Message {
|
|
47
|
+
function convertToEiMessage(msg: ClaudeCodeMessage, sessionId: string): Message {
|
|
43
48
|
return {
|
|
44
|
-
id: msg.id,
|
|
49
|
+
id: qualifyClaudeCodeMessage(getMachineId(), sessionId, msg.id),
|
|
45
50
|
role: msg.role === "user" ? "human" : "system",
|
|
46
51
|
content: msg.content,
|
|
47
52
|
timestamp: msg.timestamp,
|
|
@@ -51,9 +56,9 @@ function convertToEiMessage(msg: ClaudeCodeMessage): Message {
|
|
|
51
56
|
};
|
|
52
57
|
}
|
|
53
58
|
|
|
54
|
-
function convertToPreMarkedEiMessage(msg: ClaudeCodeMessage): Message {
|
|
59
|
+
function convertToPreMarkedEiMessage(msg: ClaudeCodeMessage, sessionId: string): Message {
|
|
55
60
|
return {
|
|
56
|
-
...convertToEiMessage(msg),
|
|
61
|
+
...convertToEiMessage(msg, sessionId),
|
|
57
62
|
f: true,
|
|
58
63
|
t: true,
|
|
59
64
|
p: true,
|
|
@@ -244,7 +249,7 @@ export async function importClaudeCodeSessions(
|
|
|
244
249
|
for (const msg of messages) {
|
|
245
250
|
const msgMs = new Date(msg.timestamp).getTime();
|
|
246
251
|
const isOld = cutoffMs !== null && msgMs < cutoffMs;
|
|
247
|
-
const eiMsg = isOld ? convertToPreMarkedEiMessage(msg) : convertToEiMessage(msg);
|
|
252
|
+
const eiMsg = isOld ? convertToPreMarkedEiMessage(msg, targetSession.id) : convertToEiMessage(msg, targetSession.id);
|
|
248
253
|
stateManager.messages_append(persona.id, eiMsg);
|
|
249
254
|
result.messagesImported++;
|
|
250
255
|
if (!isOld) toAnalyze.push(eiMsg);
|
|
@@ -268,6 +273,8 @@ export async function importClaudeCodeSessions(
|
|
|
268
273
|
sources: [`claudecode:${getMachineId()}:${targetSession.id}`],
|
|
269
274
|
};
|
|
270
275
|
|
|
276
|
+
queuePersonRewritePhase(stateManager);
|
|
277
|
+
queueTopicRewritePhase(stateManager);
|
|
271
278
|
const ccSettings = stateManager.getHuman().settings?.claudeCode;
|
|
272
279
|
queueAllScans(context, stateManager, {
|
|
273
280
|
extraction_model: ccSettings?.extraction_model,
|
|
@@ -9,10 +9,15 @@ import {
|
|
|
9
9
|
import { CursorReader } from "./reader.js";
|
|
10
10
|
import { isProcessRunning } from "../process-check.js";
|
|
11
11
|
import { getMachineId } from "../machine-id.js";
|
|
12
|
+
import { qualifyCursorMessage } from "../../core/utils/message-id.js";
|
|
12
13
|
import {
|
|
13
14
|
queueAllScans,
|
|
14
15
|
type ExtractionContext,
|
|
15
16
|
} from "../../core/orchestrators/human-extraction.js";
|
|
17
|
+
import {
|
|
18
|
+
queuePersonRewritePhase,
|
|
19
|
+
queueTopicRewritePhase,
|
|
20
|
+
} from "../../core/orchestrators/ceremony.js";
|
|
16
21
|
|
|
17
22
|
export interface CursorImportResult {
|
|
18
23
|
sessionsProcessed: number;
|
|
@@ -31,9 +36,9 @@ export interface CursorImporterOptions {
|
|
|
31
36
|
const TWELVE_HOURS_MS = 43_200_000;
|
|
32
37
|
const CURSOR_GROUP = "Cursor";
|
|
33
38
|
|
|
34
|
-
function convertToEiMessage(msg: CursorMessage): Message {
|
|
39
|
+
function convertToEiMessage(msg: CursorMessage, sessionId: string): Message {
|
|
35
40
|
return {
|
|
36
|
-
id: msg.id,
|
|
41
|
+
id: qualifyCursorMessage(getMachineId(), sessionId, msg.id),
|
|
37
42
|
role: msg.type === 1 ? "human" : "system",
|
|
38
43
|
content: msg.text,
|
|
39
44
|
timestamp: msg.timestamp,
|
|
@@ -43,9 +48,9 @@ function convertToEiMessage(msg: CursorMessage): Message {
|
|
|
43
48
|
};
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
function convertToPreMarkedEiMessage(msg: CursorMessage): Message {
|
|
51
|
+
function convertToPreMarkedEiMessage(msg: CursorMessage, sessionId: string): Message {
|
|
47
52
|
return {
|
|
48
|
-
...convertToEiMessage(msg),
|
|
53
|
+
...convertToEiMessage(msg, sessionId),
|
|
49
54
|
f: true,
|
|
50
55
|
t: true,
|
|
51
56
|
p: true,
|
|
@@ -204,7 +209,7 @@ export async function importCursorSessions(
|
|
|
204
209
|
for (const msg of messages) {
|
|
205
210
|
const msgMs = new Date(msg.timestamp).getTime();
|
|
206
211
|
const isOld = cutoffMs !== null && msgMs < cutoffMs;
|
|
207
|
-
const eiMsg = isOld ? convertToPreMarkedEiMessage(msg) : convertToEiMessage(msg);
|
|
212
|
+
const eiMsg = isOld ? convertToPreMarkedEiMessage(msg, targetSession.id) : convertToEiMessage(msg, targetSession.id);
|
|
208
213
|
stateManager.messages_append(persona.id, eiMsg);
|
|
209
214
|
result.messagesImported++;
|
|
210
215
|
if (!isOld) toAnalyze.push(eiMsg);
|
|
@@ -227,6 +232,8 @@ export async function importCursorSessions(
|
|
|
227
232
|
sources: [`cursor:${getMachineId()}:${targetSession.id}`],
|
|
228
233
|
};
|
|
229
234
|
|
|
235
|
+
queuePersonRewritePhase(stateManager);
|
|
236
|
+
queueTopicRewritePhase(stateManager);
|
|
230
237
|
queueAllScans(context, stateManager, { external_filter: "only" });
|
|
231
238
|
result.extractionScansQueued += 4;
|
|
232
239
|
}
|
|
@@ -42,7 +42,7 @@ export async function importDocument(options: DocumentImportOptions): Promise<Do
|
|
|
42
42
|
const sourceTag = `import:document:${filename}`;
|
|
43
43
|
const existingMsgs = stateManager.messages_get("emmet");
|
|
44
44
|
const staleIds = existingMsgs
|
|
45
|
-
.filter(m => m.external === true && m.
|
|
45
|
+
.filter(m => m.external === true && m.id.startsWith(`${sourceTag}:`))
|
|
46
46
|
.map(m => m.id);
|
|
47
47
|
if (staleIds.length > 0) {
|
|
48
48
|
stateManager.messages_remove("emmet", staleIds);
|
|
@@ -63,15 +63,10 @@ export function previewUnsource(sourceTag: string, stateManager: StateManager):
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
const
|
|
67
|
-
const sourceMessageIds = new Set(
|
|
68
|
-
emmettMessages
|
|
69
|
-
.filter(m => m.source_tag === sourceTag)
|
|
70
|
-
.map(m => m.id)
|
|
71
|
-
);
|
|
66
|
+
const msgIdPrefix = `${sourceTag}:`;
|
|
72
67
|
|
|
73
68
|
for (const quote of human.quotes) {
|
|
74
|
-
if (quote.message_id
|
|
69
|
+
if (quote.message_id?.startsWith(msgIdPrefix)) {
|
|
75
70
|
preview.toDelete.quotes.push({ id: quote.id, text: quote.text });
|
|
76
71
|
}
|
|
77
72
|
}
|
|
@@ -143,20 +138,22 @@ export async function executeUnsource(
|
|
|
143
138
|
stateManager.setHuman(human);
|
|
144
139
|
}
|
|
145
140
|
|
|
146
|
-
const
|
|
147
|
-
.filter(m => m.
|
|
141
|
+
const idsToRemove = stateManager.messages_get("emmet")
|
|
142
|
+
.filter(m => m.id.startsWith(`${preview.sourceTag}:`))
|
|
148
143
|
.map(m => m.id);
|
|
149
|
-
if (
|
|
150
|
-
stateManager.messages_remove("emmet",
|
|
144
|
+
if (idsToRemove.length > 0) {
|
|
145
|
+
stateManager.messages_remove("emmet", idsToRemove);
|
|
151
146
|
}
|
|
152
147
|
|
|
153
|
-
const
|
|
148
|
+
const key = preview.sourceTag.startsWith("import:document:")
|
|
154
149
|
? preview.sourceTag.slice("import:document:".length)
|
|
155
|
-
: preview.sourceTag
|
|
150
|
+
: preview.sourceTag.startsWith("generate:document:")
|
|
151
|
+
? preview.sourceTag.slice("generate:document:".length)
|
|
152
|
+
: preview.sourceTag;
|
|
156
153
|
|
|
157
154
|
const human = stateManager.getHuman();
|
|
158
155
|
if (human.settings?.document?.processed_documents) {
|
|
159
|
-
delete human.settings.document.processed_documents[
|
|
156
|
+
delete human.settings.document.processed_documents[key];
|
|
160
157
|
stateManager.setHuman(human);
|
|
161
158
|
}
|
|
162
159
|
|
|
@@ -8,8 +8,13 @@ import {
|
|
|
8
8
|
queueAllScans,
|
|
9
9
|
type ExtractionContext,
|
|
10
10
|
} from "../../core/orchestrators/human-extraction.js";
|
|
11
|
+
import {
|
|
12
|
+
queuePersonRewritePhase,
|
|
13
|
+
queueTopicRewritePhase,
|
|
14
|
+
} from "../../core/orchestrators/ceremony.js";
|
|
11
15
|
import { isProcessRunning } from "../process-check.js";
|
|
12
16
|
import { getMachineId } from "../machine-id.js";
|
|
17
|
+
import { qualifyOpenCodeMessage } from "../../core/utils/message-id.js";
|
|
13
18
|
|
|
14
19
|
// =============================================================================
|
|
15
20
|
// Constants
|
|
@@ -43,9 +48,9 @@ function isAgentToAgentMessage(content: string): boolean {
|
|
|
43
48
|
return AGENT_TO_AGENT_PREFIXES.some(prefix => trimmed.startsWith(prefix));
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
function convertToEiMessage(ocMsg: OpenCodeMessage): Message {
|
|
51
|
+
function convertToEiMessage(ocMsg: OpenCodeMessage, sessionId: string): Message {
|
|
47
52
|
return {
|
|
48
|
-
id: ocMsg.id,
|
|
53
|
+
id: qualifyOpenCodeMessage(getMachineId(), sessionId, ocMsg.id),
|
|
49
54
|
role: ocMsg.role === "user" ? "human" : "system",
|
|
50
55
|
content: ocMsg.content,
|
|
51
56
|
timestamp: ocMsg.timestamp,
|
|
@@ -55,9 +60,9 @@ function convertToEiMessage(ocMsg: OpenCodeMessage): Message {
|
|
|
55
60
|
};
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
function convertToPreMarkedEiMessage(ocMsg: OpenCodeMessage): Message {
|
|
63
|
+
function convertToPreMarkedEiMessage(ocMsg: OpenCodeMessage, sessionId: string): Message {
|
|
59
64
|
return {
|
|
60
|
-
...convertToEiMessage(ocMsg),
|
|
65
|
+
...convertToEiMessage(ocMsg, sessionId),
|
|
61
66
|
f: true,
|
|
62
67
|
t: true,
|
|
63
68
|
p: true,
|
|
@@ -195,6 +200,7 @@ export async function importOpenCodeSessions(
|
|
|
195
200
|
|
|
196
201
|
const cutoffIso = processedSessions[targetSession.id] ?? null;
|
|
197
202
|
const cutoffMs = cutoffIso ? new Date(cutoffIso).getTime() : null;
|
|
203
|
+
let anyPersonaHasChanges = false;
|
|
198
204
|
|
|
199
205
|
for (const [, { persona, msgs: agentMsgs, isNew, agentName }] of byPersonaId) {
|
|
200
206
|
if (isNew) {
|
|
@@ -227,7 +233,7 @@ export async function importOpenCodeSessions(
|
|
|
227
233
|
for (const ocMsg of agentMsgs) {
|
|
228
234
|
const msgMs = new Date(ocMsg.timestamp).getTime();
|
|
229
235
|
const isOld = cutoffMs !== null && msgMs < cutoffMs;
|
|
230
|
-
const eiMsg = isOld ? convertToPreMarkedEiMessage(ocMsg) : convertToEiMessage(ocMsg);
|
|
236
|
+
const eiMsg = isOld ? convertToPreMarkedEiMessage(ocMsg, targetSession.id) : convertToEiMessage(ocMsg, targetSession.id);
|
|
231
237
|
stateManager.messages_append(persona.id, eiMsg);
|
|
232
238
|
result.messagesImported++;
|
|
233
239
|
if (!isOld) toAnalyze.push(eiMsg);
|
|
@@ -252,6 +258,7 @@ export async function importOpenCodeSessions(
|
|
|
252
258
|
};
|
|
253
259
|
|
|
254
260
|
if (!signal?.aborted) {
|
|
261
|
+
anyPersonaHasChanges = true;
|
|
255
262
|
const openCodeSettings = stateManager.getHuman().settings?.opencode;
|
|
256
263
|
queueAllScans(context, stateManager, {
|
|
257
264
|
extraction_model: openCodeSettings?.extraction_model,
|
|
@@ -264,7 +271,13 @@ export async function importOpenCodeSessions(
|
|
|
264
271
|
|
|
265
272
|
result.sessionsProcessed = 1;
|
|
266
273
|
|
|
267
|
-
// ─── Step 6:
|
|
274
|
+
// ─── Step 6: Queue rewrite checks if any persona had new messages ─────
|
|
275
|
+
if (anyPersonaHasChanges && !signal?.aborted) {
|
|
276
|
+
queuePersonRewritePhase(stateManager);
|
|
277
|
+
queueTopicRewritePhase(stateManager);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ─── Step 7: Advance extraction state ────────────────────────────────
|
|
268
281
|
updateExtractionState(stateManager, targetSession);
|
|
269
282
|
|
|
270
283
|
console.log(
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
OpenCodeSessionRaw,
|
|
5
5
|
OpenCodeMessage,
|
|
6
6
|
OpenCodeMessageRaw,
|
|
7
|
+
OpenCodeMessageWindow,
|
|
7
8
|
OpenCodePartRaw,
|
|
8
9
|
OpenCodeAgent,
|
|
9
10
|
} from "./types.js";
|
|
@@ -208,6 +209,70 @@ export class JsonReader implements IOpenCodeReader {
|
|
|
208
209
|
);
|
|
209
210
|
}
|
|
210
211
|
|
|
212
|
+
async getMessageById(messageId: string, before = 0, after = 0): Promise<OpenCodeMessageWindow | null> {
|
|
213
|
+
if (!(await this.init())) return null;
|
|
214
|
+
|
|
215
|
+
const messageBaseDir = _join(this.storagePath!, "message");
|
|
216
|
+
let sessionDirs: string[];
|
|
217
|
+
try {
|
|
218
|
+
sessionDirs = await _readdir(messageBaseDir);
|
|
219
|
+
} catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for (const sessionId of sessionDirs) {
|
|
224
|
+
if (sessionId.startsWith(".")) continue;
|
|
225
|
+
const sessionMsgDir = _join(messageBaseDir, sessionId);
|
|
226
|
+
let files: string[];
|
|
227
|
+
try {
|
|
228
|
+
files = await _readdir(sessionMsgDir);
|
|
229
|
+
} catch {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (!files.includes(`${messageId}.json`)) continue;
|
|
233
|
+
|
|
234
|
+
const allMessages = await this.getMessagesForSession(sessionId);
|
|
235
|
+
const idx = allMessages.findIndex(m => m.id === messageId);
|
|
236
|
+
if (idx === -1) return null;
|
|
237
|
+
|
|
238
|
+
const sessionFilePath = await this.findSessionFile(sessionId);
|
|
239
|
+
const sessionRaw = sessionFilePath ? await this.readJsonFile<OpenCodeSessionRaw>(sessionFilePath) : null;
|
|
240
|
+
const session: OpenCodeSession = sessionRaw
|
|
241
|
+
? { id: sessionRaw.id, title: sessionRaw.title, directory: sessionRaw.directory, projectId: sessionRaw.projectID, parentId: sessionRaw.parentID, time: { created: sessionRaw.time.created, updated: sessionRaw.time.updated } }
|
|
242
|
+
: { id: sessionId, title: "", directory: "", projectId: "", time: { created: 0, updated: 0 } };
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
message: allMessages[idx],
|
|
246
|
+
before: allMessages.slice(Math.max(0, idx - before), idx),
|
|
247
|
+
after: allMessages.slice(idx + 1, idx + 1 + after),
|
|
248
|
+
session,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private async findSessionFile(sessionId: string): Promise<string | null> {
|
|
256
|
+
const sessionDir = _join(this.storagePath!, "session");
|
|
257
|
+
let projectDirs: string[];
|
|
258
|
+
try {
|
|
259
|
+
projectDirs = await _readdir(sessionDir);
|
|
260
|
+
} catch {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
for (const projectHash of projectDirs) {
|
|
264
|
+
if (projectHash.startsWith(".")) continue;
|
|
265
|
+
const candidate = _join(sessionDir, projectHash, `${sessionId}.json`);
|
|
266
|
+
try {
|
|
267
|
+
await _readFile(candidate, "utf-8");
|
|
268
|
+
return candidate;
|
|
269
|
+
} catch {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
211
276
|
async getAgentInfo(agentName: string): Promise<OpenCodeAgent | null> {
|
|
212
277
|
const normalized = agentName.toLowerCase();
|
|
213
278
|
if (BUILTIN_AGENTS[normalized]) {
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
IOpenCodeReader,
|
|
4
4
|
OpenCodeSession,
|
|
5
5
|
OpenCodeMessage,
|
|
6
|
+
OpenCodeMessageWindow,
|
|
6
7
|
OpenCodeAgent,
|
|
7
8
|
} from "./types.js";
|
|
8
9
|
import { BUILTIN_AGENTS } from "./types.js";
|
|
@@ -153,6 +154,38 @@ export class SqliteReader implements IOpenCodeReader {
|
|
|
153
154
|
return textParts.length > 0 ? textParts.join("\n\n") : null;
|
|
154
155
|
}
|
|
155
156
|
|
|
157
|
+
async getMessageById(messageId: string, before = 0, after = 0): Promise<OpenCodeMessageWindow | null> {
|
|
158
|
+
const row = this.db
|
|
159
|
+
.query(`SELECT session_id FROM message WHERE id = ?1 LIMIT 1`)
|
|
160
|
+
.get(messageId) as { session_id: string } | null;
|
|
161
|
+
if (!row) return null;
|
|
162
|
+
|
|
163
|
+
const sessionRow = this.db
|
|
164
|
+
.query(`SELECT id, title, directory, project_id, parent_id, time_created, time_updated FROM session WHERE id = ?1 LIMIT 1`)
|
|
165
|
+
.get(row.session_id) as { id: string; title: string; directory: string; project_id: string; parent_id: string | null; time_created: number; time_updated: number } | null;
|
|
166
|
+
if (!sessionRow) return null;
|
|
167
|
+
|
|
168
|
+
const session: OpenCodeSession = {
|
|
169
|
+
id: sessionRow.id,
|
|
170
|
+
title: sessionRow.title,
|
|
171
|
+
directory: sessionRow.directory,
|
|
172
|
+
projectId: sessionRow.project_id,
|
|
173
|
+
parentId: sessionRow.parent_id ?? undefined,
|
|
174
|
+
time: { created: sessionRow.time_created, updated: sessionRow.time_updated },
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const allMessages = await this.getMessagesForSession(row.session_id);
|
|
178
|
+
const idx = allMessages.findIndex(m => m.id === messageId);
|
|
179
|
+
if (idx === -1) return null;
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
message: allMessages[idx],
|
|
183
|
+
before: allMessages.slice(Math.max(0, idx - before), idx),
|
|
184
|
+
after: allMessages.slice(idx + 1, idx + 1 + after),
|
|
185
|
+
session,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
156
189
|
async getAgentInfo(agentName: string): Promise<OpenCodeAgent | null> {
|
|
157
190
|
const normalized = agentName.toLowerCase();
|
|
158
191
|
if (BUILTIN_AGENTS[normalized]) {
|
|
@@ -14,10 +14,18 @@
|
|
|
14
14
|
* Common interface for reading OpenCode data.
|
|
15
15
|
* Implemented by both JsonReader (legacy) and SqliteReader (1.2+).
|
|
16
16
|
*/
|
|
17
|
+
export interface OpenCodeMessageWindow {
|
|
18
|
+
message: OpenCodeMessage;
|
|
19
|
+
before: OpenCodeMessage[];
|
|
20
|
+
after: OpenCodeMessage[];
|
|
21
|
+
session: OpenCodeSession;
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
export interface IOpenCodeReader {
|
|
18
25
|
getSessionsUpdatedSince(since: Date): Promise<OpenCodeSession[]>;
|
|
19
26
|
getSessionsInRange(from: Date, to: Date): Promise<OpenCodeSession[]>;
|
|
20
27
|
getMessagesForSession(sessionId: string, since?: Date): Promise<OpenCodeMessage[]>;
|
|
28
|
+
getMessageById(messageId: string, before?: number, after?: number): Promise<OpenCodeMessageWindow | null>;
|
|
21
29
|
getAgentInfo(agentName: string): Promise<OpenCodeAgent | null>;
|
|
22
30
|
getAllUniqueAgents(sessionId: string): Promise<string[]>;
|
|
23
31
|
getFirstAgent(sessionId: string): Promise<string | null>;
|
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
queueFactFind,
|
|
7
7
|
type ExtractionContext,
|
|
8
8
|
} from "../../core/orchestrators/human-extraction.js";
|
|
9
|
+
import {
|
|
10
|
+
queuePersonRewritePhase,
|
|
11
|
+
queueTopicRewritePhase,
|
|
12
|
+
} from "../../core/orchestrators/ceremony.js";
|
|
9
13
|
|
|
10
14
|
export interface PersonaHistoryImportResult {
|
|
11
15
|
daysQueued: number;
|
|
@@ -141,6 +145,11 @@ export async function importPersonaHistory(
|
|
|
141
145
|
|
|
142
146
|
result.daysQueued = 1;
|
|
143
147
|
|
|
148
|
+
if (result.scansQueued > 0) {
|
|
149
|
+
queuePersonRewritePhase(stateManager);
|
|
150
|
+
queueTopicRewritePhase(stateManager);
|
|
151
|
+
}
|
|
152
|
+
|
|
144
153
|
const isLastDay = currentDate >= today;
|
|
145
154
|
advanceProgress(stateManager, currentDate, isLastDay);
|
|
146
155
|
|
|
@@ -45,7 +45,7 @@ Rules:
|
|
|
45
45
|
- Be specific: "React performance patterns" beats "technical stuff"
|
|
46
46
|
- If the record is clean — everything in it passes the test — return an empty array
|
|
47
47
|
|
|
48
|
-
Return a raw JSON array of strings. No markdown fencing, no commentary.
|
|
48
|
+
Return a raw JSON array of strings. No markdown fencing, no commentary. Thinking text WILL break the parser.
|
|
49
49
|
|
|
50
50
|
Example — a Person named "Nicholas" whose description includes sprint ticket numbers:
|
|
51
51
|
["CMIDP sprint ticket assignments", "ASU Data Lake access provisioning details"]`;
|
|
@@ -60,7 +60,7 @@ Example — a Person named "Nicholas" whose description includes sprint ticket n
|
|
|
60
60
|
|
|
61
61
|
---
|
|
62
62
|
|
|
63
|
-
Return a raw JSON array of subject phrases found in this Person record that don't belong there. Return [] if the record is clean.`;
|
|
63
|
+
Return a raw JSON array of subject phrases found in this Person record that don't belong there. Return [] if the record is clean. Thinking text WILL break the parser.`;
|
|
64
64
|
|
|
65
65
|
return { system, user };
|
|
66
66
|
}
|
|
@@ -35,7 +35,7 @@ Rules:
|
|
|
35
35
|
- Be specific: "TypeScript coding conventions" beats "technical preferences"
|
|
36
36
|
- If the record is cohesive and on-topic despite its length, return an empty array
|
|
37
37
|
${technicalGuidance}
|
|
38
|
-
Return a raw JSON array of strings. No markdown fencing, no commentary.
|
|
38
|
+
Return a raw JSON array of strings. No markdown fencing, no commentary. Thinking text WILL break the parser.
|
|
39
39
|
|
|
40
40
|
Example — a Topic named "Software Engineering" whose description also discusses vim keybindings, git conventions, and AI tooling:
|
|
41
41
|
["vim keybindings and editor configuration", "git and GitHub workflow conventions", "AI coding assistant preferences"]`;
|
|
@@ -51,7 +51,7 @@ Example — a Topic named "Software Engineering" whose description also discusse
|
|
|
51
51
|
]
|
|
52
52
|
\`\`\`
|
|
53
53
|
|
|
54
|
-
Respond with raw JSON array only.`;
|
|
54
|
+
Respond with raw JSON array only. Thinking text WILL break the parser.`;
|
|
55
55
|
|
|
56
56
|
const user = `${payload}
|
|
57
57
|
|
|
@@ -9,6 +9,7 @@ import type { HeartbeatCheckPromptData, PromptOutput } from "./types.js";
|
|
|
9
9
|
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
|
+
import { partitionTraits } from "../trait-utils.js";
|
|
12
13
|
function formatTopicsWithGaps(topics: Topic[]): string {
|
|
13
14
|
if (topics.length === 0) return "(No topics with engagement gaps)";
|
|
14
15
|
|
|
@@ -85,13 +86,15 @@ export function buildHeartbeatCheckPrompt(data: HeartbeatCheckPromptData): Promp
|
|
|
85
86
|
|
|
86
87
|
You are NOT having a conversation right now - you are deciding IF you should start one.`;
|
|
87
88
|
|
|
89
|
+
const { active: activeTraits } = partitionTraits(data.persona.traits);
|
|
90
|
+
|
|
88
91
|
const contextFragment = `## Context
|
|
89
92
|
|
|
90
93
|
It has been ${data.inactive_days} day${data.inactive_days !== 1 ? 's' : ''} since your last interaction.
|
|
91
94
|
|
|
92
95
|
### Your Personality
|
|
93
|
-
${
|
|
94
|
-
?
|
|
96
|
+
${activeTraits.length > 0
|
|
97
|
+
? activeTraits.map(t => `- **${t.name}**: ${t.description}`).join('\n')
|
|
95
98
|
: "(No specific traits defined)"}
|
|
96
99
|
|
|
97
100
|
### Topics You Care About
|
|
@@ -41,6 +41,12 @@ function formatItem(item: EiHeartbeatItem): string {
|
|
|
41
41
|
` ${item.critique}`,
|
|
42
42
|
].join("\n");
|
|
43
43
|
|
|
44
|
+
case "Self Reflection Alert":
|
|
45
|
+
return [
|
|
46
|
+
`- **${item.id}** Self Reflection Alert (your own identity)`,
|
|
47
|
+
` ${item.critique}`,
|
|
48
|
+
].join("\n");
|
|
49
|
+
|
|
44
50
|
default:
|
|
45
51
|
return '';
|
|
46
52
|
}
|
|
@@ -86,6 +92,7 @@ ${itemsSection}
|
|
|
86
92
|
- **Low-Engagement Person / Topic**: Write a natural, warm message that naturally brings up this person or topic. Set the id and my_response.
|
|
87
93
|
- **Inactive Persona**: Write a message that gently mentions the persona might be worth checking in with. Set the id and my_response.
|
|
88
94
|
- **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
|
+
- **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.
|
|
89
96
|
|
|
90
97
|
## When NOT to Reach Out
|
|
91
98
|
|