ei-tui 1.6.2 → 1.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli/README.md +2 -0
- package/src/cli/install.ts +708 -0
- package/src/cli/retrieval.ts +22 -0
- package/src/cli/session-context.ts +98 -0
- package/src/cli.ts +2 -669
- package/src/core/bootstrap-tools.ts +486 -0
- package/src/core/handlers/document-segmentation.ts +1 -2
- package/src/core/handlers/heartbeat.ts +3 -2
- package/src/core/handlers/persona-response.ts +5 -4
- package/src/core/handlers/rooms.ts +6 -5
- package/src/core/integration-sync-manager.ts +482 -0
- package/src/core/message-manager.ts +2 -1
- package/src/core/migrations.ts +297 -0
- package/src/core/orchestrators/ceremony.ts +2 -1
- package/src/core/processor.ts +17 -1151
- package/src/core/room-manager.ts +17 -4
- package/src/core/state-manager.ts +2 -1
- package/src/core/types/entities.ts +1 -0
- package/src/core/utils/message-id.ts +15 -0
- package/src/integrations/claude-code/importer.ts +9 -30
- package/src/integrations/claude-code/types.ts +1 -1
- package/src/integrations/codex/importer.ts +6 -27
- package/src/integrations/codex/types.ts +1 -1
- package/src/integrations/constants.ts +3 -0
- package/src/integrations/cursor/importer.ts +9 -26
- package/src/integrations/cursor/types.ts +1 -1
- package/src/integrations/pi/importer.ts +235 -0
- package/src/integrations/pi/index.ts +3 -0
- package/src/integrations/pi/reader.ts +247 -0
- package/src/integrations/pi/types.ts +151 -0
- package/src/integrations/shared/message-converter.ts +41 -0
- package/src/integrations/slack/importer.ts +1 -1
- package/tui/README.md +1 -0
- package/tui/src/components/PromptInput.tsx +5 -1
- package/tui/src/util/yaml-settings.ts +28 -0
package/src/core/room-manager.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ContextStatus, LLMNextStep, LLMPriority, LLMRequestType, RoomMode } from "./types.js";
|
|
2
2
|
import type { RoomCreationInput, RoomEntity, RoomMessage, RoomSummary, EiError } from "./types.js";
|
|
3
|
+
import { qualifyEiMessage } from "./utils/message-id.js";
|
|
3
4
|
import type { StateManager } from "./state-manager.js";
|
|
4
5
|
import { buildRoomResponsePromptData } from "./prompt-context-builder.js";
|
|
5
6
|
import { buildRoomJudgePrompt } from "../prompts/room/index.js";
|
|
@@ -117,7 +118,7 @@ export function submitHumanRoomMessage(
|
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
const msg: RoomMessage = {
|
|
120
|
-
id: crypto.randomUUID(),
|
|
121
|
+
id: qualifyEiMessage(crypto.randomUUID()),
|
|
121
122
|
parent_id: room.active_node_id,
|
|
122
123
|
role: "human",
|
|
123
124
|
content: content ?? undefined,
|
|
@@ -160,12 +161,17 @@ export async function sendFfaMessage(
|
|
|
160
161
|
}
|
|
161
162
|
const ffaParentId = ffaRootMsg.id;
|
|
162
163
|
|
|
163
|
-
const
|
|
164
|
+
const allMessages = sm.getRoomMessages(roomId);
|
|
165
|
+
const existing = allMessages.find(
|
|
164
166
|
m => m.role === "human" && m.id === room.active_node_id && m.parent_id === ffaParentId
|
|
165
167
|
);
|
|
166
168
|
|
|
167
169
|
let humanMsgId: string;
|
|
168
|
-
|
|
170
|
+
const existingHasChildren = existing
|
|
171
|
+
? allMessages.some(m => m.parent_id === existing.id && m.role === "persona")
|
|
172
|
+
: false;
|
|
173
|
+
|
|
174
|
+
if (existing && !existingHasChildren) {
|
|
169
175
|
sm.updateRoomMessage(roomId, existing.id, {
|
|
170
176
|
content: content ?? undefined,
|
|
171
177
|
silence_reason: content ? undefined : (silenceReason ?? "passed"),
|
|
@@ -174,7 +180,7 @@ export async function sendFfaMessage(
|
|
|
174
180
|
humanMsgId = existing.id;
|
|
175
181
|
} else {
|
|
176
182
|
const msg: RoomMessage = {
|
|
177
|
-
id: crypto.randomUUID(),
|
|
183
|
+
id: qualifyEiMessage(crypto.randomUUID()),
|
|
178
184
|
parent_id: ffaParentId,
|
|
179
185
|
role: "human",
|
|
180
186
|
content: content ?? undefined,
|
|
@@ -192,6 +198,12 @@ export async function sendFfaMessage(
|
|
|
192
198
|
onRoomUpdated(roomId);
|
|
193
199
|
|
|
194
200
|
const updatedRoom = sm.getRoom(roomId)!;
|
|
201
|
+
const alreadyAnswered = new Set(
|
|
202
|
+
sm.getRoomMessages(roomId)
|
|
203
|
+
.filter(m => m.parent_id === humanMsgId && m.role === "persona" && m.persona_id)
|
|
204
|
+
.map(m => m.persona_id!)
|
|
205
|
+
);
|
|
206
|
+
|
|
195
207
|
const alreadyQueued = new Set(
|
|
196
208
|
sm.queue_getAllActiveItems()
|
|
197
209
|
.filter(q =>
|
|
@@ -205,6 +217,7 @@ export async function sendFfaMessage(
|
|
|
205
217
|
const shuffledIds = [...updatedRoom.persona_ids].sort(() => Math.random() - 0.5);
|
|
206
218
|
|
|
207
219
|
for (const personaId of shuffledIds) {
|
|
220
|
+
if (alreadyAnswered.has(personaId)) continue;
|
|
208
221
|
if (alreadyQueued.has(personaId)) continue;
|
|
209
222
|
const persona = sm.persona_getById(personaId);
|
|
210
223
|
if (!persona || persona.is_archived || persona.is_paused) continue;
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
} from "./types.js";
|
|
20
20
|
import { RoomMode } from "./types.js";
|
|
21
21
|
import { BUILT_IN_FACT_NAMES } from './constants/built-in-facts.js';
|
|
22
|
+
import { qualifyEiMessage } from './utils/message-id.js';
|
|
22
23
|
import type { ThemeDefinition } from './types/entities.js';
|
|
23
24
|
import type { Storage } from "../storage/interface.js";
|
|
24
25
|
import {
|
|
@@ -446,7 +447,7 @@ export class StateManager {
|
|
|
446
447
|
addRoom(input: RoomCreationInput): RoomEntity {
|
|
447
448
|
const now = new Date().toISOString();
|
|
448
449
|
const initialMessage: RoomMessage = {
|
|
449
|
-
id: crypto.randomUUID(),
|
|
450
|
+
id: qualifyEiMessage(crypto.randomUUID()),
|
|
450
451
|
parent_id: null,
|
|
451
452
|
role: "human",
|
|
452
453
|
content: input.initial_message,
|
|
@@ -131,6 +131,7 @@ export interface HumanSettings {
|
|
|
131
131
|
claudeCode?: import("../../integrations/claude-code/types.js").ClaudeCodeSettings;
|
|
132
132
|
cursor?: import("../../integrations/cursor/types.js").CursorSettings;
|
|
133
133
|
codex?: import("../../integrations/codex/types.js").CodexSettings;
|
|
134
|
+
pi?: import("../../integrations/pi/types.js").PiSettings;
|
|
134
135
|
document?: DocumentSettings;
|
|
135
136
|
active_theme?: string;
|
|
136
137
|
custom_themes?: ThemeDefinition[];
|
|
@@ -15,6 +15,7 @@ export type MessageIdIntegration =
|
|
|
15
15
|
| "claudecode"
|
|
16
16
|
| "cursor"
|
|
17
17
|
| "codex"
|
|
18
|
+
| "pi"
|
|
18
19
|
| "import"
|
|
19
20
|
| "slack"
|
|
20
21
|
| "unknown"
|
|
@@ -79,6 +80,16 @@ export function parseMessageId(id: string): ParsedMessageId {
|
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
if (parts[0] === "pi" && parts.length >= 4) {
|
|
84
|
+
return {
|
|
85
|
+
integration: "pi",
|
|
86
|
+
machine: parts[1],
|
|
87
|
+
session: parts[2],
|
|
88
|
+
nativeId: parts.slice(3).join(":"),
|
|
89
|
+
raw: id,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
82
93
|
if (parts[0] === "import" && parts[1] === "document" && parts.length >= 4) {
|
|
83
94
|
return {
|
|
84
95
|
integration: "import",
|
|
@@ -125,6 +136,10 @@ export function qualifyCodexMessage(machine: string, sessionId: string, nativeId
|
|
|
125
136
|
return `codex:${machine}:${sessionId}:${nativeId}`
|
|
126
137
|
}
|
|
127
138
|
|
|
139
|
+
export function qualifyPiMessage(machine: string, sessionId: string, nativeId: string): string {
|
|
140
|
+
return `pi:${machine}:${sessionId}:${nativeId}`
|
|
141
|
+
}
|
|
142
|
+
|
|
128
143
|
export function qualifyDocumentMessage(slug: string, uuid: string): string {
|
|
129
144
|
return `import:document:${slug}:${uuid}`
|
|
130
145
|
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import type { StateManager } from "../../core/state-manager.js";
|
|
2
|
-
import type { Ei_Interface, Message,
|
|
2
|
+
import type { Ei_Interface, Message, PersonaEntity, PersonaTrait } from "../../core/types.js";
|
|
3
3
|
import { DEFAULT_SEED_TRAITS } from "../../core/constants/seed-traits.js";
|
|
4
|
-
import type { IClaudeCodeReader, ClaudeCodeSession
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
MIN_SESSION_AGE_MS,
|
|
8
|
-
} from "./types.js";
|
|
4
|
+
import type { IClaudeCodeReader, ClaudeCodeSession } from "./types.js";
|
|
5
|
+
import { CLAUDE_CODE_PERSONA_NAME } from "./types.js";
|
|
6
|
+
import { MIN_SESSION_AGE_MS } from "../constants.js";
|
|
9
7
|
import { ClaudeCodeReader } from "./reader.js";
|
|
10
8
|
import {
|
|
11
9
|
queueAllScans,
|
|
@@ -16,8 +14,10 @@ import {
|
|
|
16
14
|
queueTopicRewritePhase,
|
|
17
15
|
} from "../../core/orchestrators/ceremony.js";
|
|
18
16
|
import { isProcessRunning } from "../process-check.js";
|
|
19
|
-
import { getMachineId } from "../machine-id.js";
|
|
20
17
|
import { qualifyClaudeCodeMessage } from "../../core/utils/message-id.js";
|
|
18
|
+
import { getMachineId } from "../machine-id.js";
|
|
19
|
+
import { convertToEiMessage, convertToPreMarkedEiMessage } from "../shared/message-converter.js";
|
|
20
|
+
import { TWELVE_HOURS_MS } from "../constants.js";
|
|
21
21
|
|
|
22
22
|
// =============================================================================
|
|
23
23
|
// Export Types
|
|
@@ -41,30 +41,9 @@ export interface ClaudeCodeImporterOptions {
|
|
|
41
41
|
// Utility Functions
|
|
42
42
|
// =============================================================================
|
|
43
43
|
|
|
44
|
-
const TWELVE_HOURS_MS = 43_200_000;
|
|
45
44
|
const CLAUDE_CODE_GROUP = "Claude Code";
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
id: qualifyClaudeCodeMessage(getMachineId(), sessionId, msg.id),
|
|
50
|
-
role: msg.role === "user" ? "human" : "system",
|
|
51
|
-
content: msg.content,
|
|
52
|
-
timestamp: msg.timestamp,
|
|
53
|
-
read: true,
|
|
54
|
-
context_status: "default" as ContextStatus,
|
|
55
|
-
external: true,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function convertToPreMarkedEiMessage(msg: ClaudeCodeMessage, sessionId: string): Message {
|
|
60
|
-
return {
|
|
61
|
-
...convertToEiMessage(msg, sessionId),
|
|
62
|
-
f: true,
|
|
63
|
-
t: true,
|
|
64
|
-
p: true,
|
|
65
|
-
e: true,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
46
|
+
const qualify = qualifyClaudeCodeMessage;
|
|
68
47
|
|
|
69
48
|
/**
|
|
70
49
|
* Ensure the single "Claude Code" persona exists.
|
|
@@ -249,7 +228,7 @@ export async function importClaudeCodeSessions(
|
|
|
249
228
|
for (const msg of messages) {
|
|
250
229
|
const msgMs = new Date(msg.timestamp).getTime();
|
|
251
230
|
const isOld = cutoffMs !== null && msgMs < cutoffMs;
|
|
252
|
-
const eiMsg = isOld ? convertToPreMarkedEiMessage(msg, targetSession.id) : convertToEiMessage(msg, targetSession.id);
|
|
231
|
+
const eiMsg = isOld ? convertToPreMarkedEiMessage(msg, targetSession.id, qualify) : convertToEiMessage(msg, targetSession.id, qualify);
|
|
253
232
|
stateManager.messages_append(persona.id, eiMsg);
|
|
254
233
|
result.messagesImported++;
|
|
255
234
|
if (!isOld) toAnalyze.push(eiMsg);
|
|
@@ -139,7 +139,7 @@ export const CLAUDE_CODE_TOPIC_GROUPS = ["General", "Coding", "Claude Code"];
|
|
|
139
139
|
* Minimum session age before we import it.
|
|
140
140
|
* Mirrors OpenCode's 20-minute rule — gives the session time to "settle."
|
|
141
141
|
*/
|
|
142
|
-
export
|
|
142
|
+
export { MIN_SESSION_AGE_MS } from "../constants.js";
|
|
143
143
|
|
|
144
144
|
// ============================================================================
|
|
145
145
|
// Human Settings Shape (mirrors OpenCodeSettings in core/types.ts)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { StateManager } from "../../core/state-manager.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { Ei_Interface, Message, PersonaEntity, PersonaTrait } from "../../core/types.js";
|
|
3
3
|
import { DEFAULT_SEED_TRAITS } from "../../core/constants/seed-traits.js";
|
|
4
4
|
import {
|
|
5
5
|
queueAllScans,
|
|
@@ -10,16 +10,16 @@ import {
|
|
|
10
10
|
queueTopicRewritePhase,
|
|
11
11
|
} from "../../core/orchestrators/ceremony.js";
|
|
12
12
|
import { qualifyCodexMessage } from "../../core/utils/message-id.js";
|
|
13
|
+
import { convertToEiMessage, convertToPreMarkedEiMessage } from "../shared/message-converter.js";
|
|
13
14
|
import { getMachineId } from "../machine-id.js";
|
|
14
15
|
import { isProcessRunning } from "../process-check.js";
|
|
15
16
|
import { CodexReader } from "./reader.js";
|
|
16
17
|
import {
|
|
17
18
|
CODEX_PERSONA_NAME,
|
|
18
|
-
MIN_SESSION_AGE_MS,
|
|
19
|
-
type CodexMessage,
|
|
20
19
|
type CodexSession,
|
|
21
20
|
type ICodexReader,
|
|
22
21
|
} from "./types.js";
|
|
22
|
+
import { MIN_SESSION_AGE_MS, TWELVE_HOURS_MS } from "../constants.js";
|
|
23
23
|
|
|
24
24
|
export interface CodexImportResult {
|
|
25
25
|
sessionsProcessed: number;
|
|
@@ -35,30 +35,9 @@ export interface CodexImporterOptions {
|
|
|
35
35
|
signal?: AbortSignal;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
const TWELVE_HOURS_MS = 43_200_000;
|
|
39
38
|
const CODEX_GROUP = "Codex";
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
id: qualifyCodexMessage(getMachineId(), sessionId, msg.id),
|
|
44
|
-
role: msg.role === "user" ? "human" : "system",
|
|
45
|
-
content: msg.content,
|
|
46
|
-
timestamp: msg.timestamp,
|
|
47
|
-
read: true,
|
|
48
|
-
context_status: "default" as ContextStatus,
|
|
49
|
-
external: true,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function convertToPreMarkedEiMessage(msg: CodexMessage, sessionId: string): Message {
|
|
54
|
-
return {
|
|
55
|
-
...convertToEiMessage(msg, sessionId),
|
|
56
|
-
f: true,
|
|
57
|
-
t: true,
|
|
58
|
-
p: true,
|
|
59
|
-
e: true,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
40
|
+
const qualify = qualifyCodexMessage;
|
|
62
41
|
|
|
63
42
|
function ensureCodexPersona(
|
|
64
43
|
stateManager: StateManager,
|
|
@@ -212,8 +191,8 @@ export async function importCodexSessions(
|
|
|
212
191
|
const msgMs = new Date(msg.timestamp).getTime();
|
|
213
192
|
const isOld = cutoffMs !== null && msgMs < cutoffMs;
|
|
214
193
|
const eiMsg = isOld
|
|
215
|
-
? convertToPreMarkedEiMessage(msg, targetSession.id)
|
|
216
|
-
: convertToEiMessage(msg, targetSession.id);
|
|
194
|
+
? convertToPreMarkedEiMessage(msg, targetSession.id, qualify)
|
|
195
|
+
: convertToEiMessage(msg, targetSession.id, qualify);
|
|
217
196
|
|
|
218
197
|
stateManager.messages_append(persona.id, eiMsg);
|
|
219
198
|
result.messagesImported++;
|
|
@@ -94,7 +94,7 @@ export const CODEX_TOPIC_GROUPS = ["General", "Coding", "Codex"];
|
|
|
94
94
|
* Minimum session age before import.
|
|
95
95
|
* Mirrors Claude Code / Cursor's 20-minute rule so active sessions can settle.
|
|
96
96
|
*/
|
|
97
|
-
export
|
|
97
|
+
export { MIN_SESSION_AGE_MS } from "../constants.js";
|
|
98
98
|
|
|
99
99
|
// ============================================================================
|
|
100
100
|
// Human Settings Shape
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import type { StateManager } from "../../core/state-manager.js";
|
|
2
|
-
import type { Ei_Interface, Message,
|
|
2
|
+
import type { Ei_Interface, Message, PersonaEntity, PersonaTrait } from "../../core/types.js";
|
|
3
3
|
import { DEFAULT_SEED_TRAITS } from "../../core/constants/seed-traits.js";
|
|
4
4
|
import type { ICursorReader, CursorSession, CursorMessage } from "./types.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
MIN_SESSION_AGE_MS,
|
|
8
|
-
} from "./types.js";
|
|
5
|
+
import { CURSOR_PERSONA_NAME } from "./types.js";
|
|
6
|
+
import { MIN_SESSION_AGE_MS, TWELVE_HOURS_MS } from "../constants.js";
|
|
9
7
|
import { CursorReader } from "./reader.js";
|
|
10
8
|
import { isProcessRunning } from "../process-check.js";
|
|
11
9
|
import { getMachineId } from "../machine-id.js";
|
|
12
10
|
import { qualifyCursorMessage } from "../../core/utils/message-id.js";
|
|
11
|
+
import { convertToEiMessage, convertToPreMarkedEiMessage } from "../shared/message-converter.js";
|
|
13
12
|
import {
|
|
14
13
|
queueAllScans,
|
|
15
14
|
type ExtractionContext,
|
|
@@ -33,29 +32,12 @@ export interface CursorImporterOptions {
|
|
|
33
32
|
signal?: AbortSignal;
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
const TWELVE_HOURS_MS = 43_200_000;
|
|
37
35
|
const CURSOR_GROUP = "Cursor";
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
id: qualifyCursorMessage(getMachineId(), sessionId, msg.id),
|
|
42
|
-
role: msg.type === 1 ? "human" : "system",
|
|
43
|
-
content: msg.text,
|
|
44
|
-
timestamp: msg.timestamp,
|
|
45
|
-
read: true,
|
|
46
|
-
context_status: "default" as ContextStatus,
|
|
47
|
-
external: true,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
37
|
+
const qualify = qualifyCursorMessage;
|
|
50
38
|
|
|
51
|
-
function
|
|
52
|
-
return {
|
|
53
|
-
...convertToEiMessage(msg, sessionId),
|
|
54
|
-
f: true,
|
|
55
|
-
t: true,
|
|
56
|
-
p: true,
|
|
57
|
-
e: true,
|
|
58
|
-
};
|
|
39
|
+
function normalizeCursorMessage(msg: CursorMessage) {
|
|
40
|
+
return { id: msg.id, role: (msg.type === 1 ? "user" : "assistant") as "user" | "assistant", content: msg.text, timestamp: msg.timestamp };
|
|
59
41
|
}
|
|
60
42
|
|
|
61
43
|
function ensureCursorPersona(
|
|
@@ -209,7 +191,8 @@ export async function importCursorSessions(
|
|
|
209
191
|
for (const msg of messages) {
|
|
210
192
|
const msgMs = new Date(msg.timestamp).getTime();
|
|
211
193
|
const isOld = cutoffMs !== null && msgMs < cutoffMs;
|
|
212
|
-
const
|
|
194
|
+
const normalized = normalizeCursorMessage(msg);
|
|
195
|
+
const eiMsg = isOld ? convertToPreMarkedEiMessage(normalized, targetSession.id, qualify) : convertToEiMessage(normalized, targetSession.id, qualify);
|
|
213
196
|
stateManager.messages_append(persona.id, eiMsg);
|
|
214
197
|
result.messagesImported++;
|
|
215
198
|
if (!isOld) toAnalyze.push(eiMsg);
|
|
@@ -116,7 +116,7 @@ export const CURSOR_TOPIC_GROUPS = ["General", "Coding", "Cursor"];
|
|
|
116
116
|
* Minimum session age before we import it.
|
|
117
117
|
* Mirrors ClaudeCode's 20-minute rule — gives the session time to "settle."
|
|
118
118
|
*/
|
|
119
|
-
export
|
|
119
|
+
export { MIN_SESSION_AGE_MS } from "../constants.js";
|
|
120
120
|
|
|
121
121
|
// ============================================================================
|
|
122
122
|
// Human Settings Shape
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import type { StateManager } from "../../core/state-manager.js";
|
|
2
|
+
import type { Ei_Interface, Message, PersonaEntity, PersonaTrait } from "../../core/types.js";
|
|
3
|
+
import { DEFAULT_SEED_TRAITS } from "../../core/constants/seed-traits.js";
|
|
4
|
+
import {
|
|
5
|
+
queueAllScans,
|
|
6
|
+
type ExtractionContext,
|
|
7
|
+
} from "../../core/orchestrators/human-extraction.js";
|
|
8
|
+
import {
|
|
9
|
+
queuePersonRewritePhase,
|
|
10
|
+
queueTopicRewritePhase,
|
|
11
|
+
} from "../../core/orchestrators/ceremony.js";
|
|
12
|
+
import { qualifyPiMessage } from "../../core/utils/message-id.js";
|
|
13
|
+
import { convertToEiMessage, convertToPreMarkedEiMessage } from "../shared/message-converter.js";
|
|
14
|
+
import { getMachineId } from "../machine-id.js";
|
|
15
|
+
import { isProcessRunning } from "../process-check.js";
|
|
16
|
+
import { PiReader } from "./reader.js";
|
|
17
|
+
import {
|
|
18
|
+
PI_PERSONA_NAME,
|
|
19
|
+
type PiSession,
|
|
20
|
+
type IPiReader,
|
|
21
|
+
} from "./types.js";
|
|
22
|
+
import { MIN_SESSION_AGE_MS, TWELVE_HOURS_MS } from "../constants.js";
|
|
23
|
+
|
|
24
|
+
export interface PiImportResult {
|
|
25
|
+
sessionsProcessed: number;
|
|
26
|
+
messagesImported: number;
|
|
27
|
+
personaCreated: boolean;
|
|
28
|
+
extractionScansQueued: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PiImporterOptions {
|
|
32
|
+
stateManager: StateManager;
|
|
33
|
+
interface?: Ei_Interface;
|
|
34
|
+
reader?: IPiReader;
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const PI_GROUP = "Pi";
|
|
39
|
+
|
|
40
|
+
const qualify = qualifyPiMessage;
|
|
41
|
+
|
|
42
|
+
function ensurePiPersona(
|
|
43
|
+
stateManager: StateManager,
|
|
44
|
+
eiInterface?: Ei_Interface
|
|
45
|
+
): PersonaEntity {
|
|
46
|
+
const existing = stateManager.persona_getByName(PI_PERSONA_NAME);
|
|
47
|
+
if (existing) return existing;
|
|
48
|
+
|
|
49
|
+
const now = new Date().toISOString();
|
|
50
|
+
const seedTraits: PersonaTrait[] = DEFAULT_SEED_TRAITS.map((t) => ({
|
|
51
|
+
id: crypto.randomUUID(),
|
|
52
|
+
name: t.name,
|
|
53
|
+
description: t.description,
|
|
54
|
+
sentiment: t.sentiment,
|
|
55
|
+
strength: t.strength,
|
|
56
|
+
last_updated: now,
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
const persona: PersonaEntity = {
|
|
60
|
+
id: crypto.randomUUID(),
|
|
61
|
+
display_name: PI_PERSONA_NAME,
|
|
62
|
+
entity: "system",
|
|
63
|
+
aliases: ["pi", "pi coding agent", "omp", "oh-my-pi"],
|
|
64
|
+
short_description: "Pi - minimal terminal coding harness",
|
|
65
|
+
long_description:
|
|
66
|
+
"Pi is a minimal terminal coding harness. Covers both vanilla Pi (earendil-works/pi) and the oh-my-pi fork (omp), which share the same JSONL session format.",
|
|
67
|
+
group_primary: PI_GROUP,
|
|
68
|
+
groups_visible: [PI_GROUP],
|
|
69
|
+
traits: seedTraits,
|
|
70
|
+
topics: [],
|
|
71
|
+
is_paused: false,
|
|
72
|
+
is_archived: false,
|
|
73
|
+
is_static: false,
|
|
74
|
+
heartbeat_delay_ms: TWELVE_HOURS_MS,
|
|
75
|
+
last_heartbeat: now,
|
|
76
|
+
last_updated: now,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
stateManager.persona_add(persona);
|
|
80
|
+
eiInterface?.onPersonaAdded?.();
|
|
81
|
+
return persona;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function updateProcessedState(stateManager: StateManager, session: PiSession): void {
|
|
85
|
+
const human = stateManager.getHuman();
|
|
86
|
+
const lastMessageMs = new Date(session.lastMessageAt).getTime();
|
|
87
|
+
const extractionPoint = human.settings?.pi?.extraction_point;
|
|
88
|
+
const currentPointMs = extractionPoint ? new Date(extractionPoint).getTime() : 0;
|
|
89
|
+
const newPointMs = Math.max(currentPointMs, lastMessageMs);
|
|
90
|
+
|
|
91
|
+
const processedSessions = {
|
|
92
|
+
...(human.settings?.pi?.processed_sessions ?? {}),
|
|
93
|
+
[session.id]: new Date().toISOString(),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
stateManager.setHuman({
|
|
97
|
+
...human,
|
|
98
|
+
settings: {
|
|
99
|
+
...human.settings,
|
|
100
|
+
pi: {
|
|
101
|
+
...human.settings?.pi,
|
|
102
|
+
extraction_point: new Date(newPointMs).toISOString(),
|
|
103
|
+
processed_sessions: processedSessions,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function isPiRunning(): Promise<boolean> {
|
|
110
|
+
return (
|
|
111
|
+
(await isProcessRunning("pi")) ||
|
|
112
|
+
(await isProcessRunning("omp"))
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function importPiSessions(options: PiImporterOptions): Promise<PiImportResult> {
|
|
117
|
+
const { stateManager, interface: eiInterface, signal } = options;
|
|
118
|
+
const reader = options.reader ?? new PiReader();
|
|
119
|
+
|
|
120
|
+
const result: PiImportResult = {
|
|
121
|
+
sessionsProcessed: 0,
|
|
122
|
+
messagesImported: 0,
|
|
123
|
+
personaCreated: false,
|
|
124
|
+
extractionScansQueued: 0,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const allSessions = await reader.getSessions();
|
|
128
|
+
if (signal?.aborted) return result;
|
|
129
|
+
|
|
130
|
+
const human = stateManager.getHuman();
|
|
131
|
+
const processedSessions = human.settings?.pi?.processed_sessions ?? {};
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
const toolRunning = await isPiRunning();
|
|
134
|
+
|
|
135
|
+
let targetSession: PiSession | null = null;
|
|
136
|
+
|
|
137
|
+
for (const session of allSessions) {
|
|
138
|
+
const sessionLastMs = new Date(session.lastMessageAt).getTime();
|
|
139
|
+
const ageMs = now - sessionLastMs;
|
|
140
|
+
|
|
141
|
+
if (ageMs < MIN_SESSION_AGE_MS && toolRunning) continue;
|
|
142
|
+
|
|
143
|
+
const lastImported = processedSessions[session.id];
|
|
144
|
+
if (lastImported && sessionLastMs <= new Date(lastImported).getTime()) continue;
|
|
145
|
+
|
|
146
|
+
targetSession = session;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!targetSession) {
|
|
151
|
+
console.log("[Pi] All sessions processed, nothing new to import");
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (signal?.aborted) return result;
|
|
156
|
+
|
|
157
|
+
console.log(
|
|
158
|
+
`[Pi] Processing session: "${targetSession.title}" ` +
|
|
159
|
+
`(last message: ${targetSession.lastMessageAt})`
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const messages = targetSession.messages;
|
|
163
|
+
if (messages.length === 0) {
|
|
164
|
+
updateProcessedState(stateManager, targetSession);
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (signal?.aborted) return result;
|
|
169
|
+
|
|
170
|
+
const personaExistedBefore = stateManager.persona_getByName(PI_PERSONA_NAME) !== null;
|
|
171
|
+
const persona = ensurePiPersona(stateManager, eiInterface);
|
|
172
|
+
result.personaCreated = !personaExistedBefore;
|
|
173
|
+
|
|
174
|
+
if (!personaExistedBefore) {
|
|
175
|
+
stateManager.persona_archive(persona.id);
|
|
176
|
+
} else {
|
|
177
|
+
const existingMsgs = stateManager.messages_get(persona.id);
|
|
178
|
+
const externalIds = existingMsgs.filter((m) => m.external === true).map((m) => m.id);
|
|
179
|
+
if (externalIds.length > 0) {
|
|
180
|
+
stateManager.messages_remove(persona.id, externalIds);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const cutoffIso = processedSessions[targetSession.id] ?? null;
|
|
185
|
+
const cutoffMs = cutoffIso ? new Date(cutoffIso).getTime() : null;
|
|
186
|
+
const toAnalyze: Message[] = [];
|
|
187
|
+
|
|
188
|
+
for (const msg of messages) {
|
|
189
|
+
const msgMs = new Date(msg.timestamp).getTime();
|
|
190
|
+
const isOld = cutoffMs !== null && msgMs < cutoffMs;
|
|
191
|
+
const eiMsg = isOld
|
|
192
|
+
? convertToPreMarkedEiMessage(msg, targetSession.id, qualify)
|
|
193
|
+
: convertToEiMessage(msg, targetSession.id, qualify);
|
|
194
|
+
|
|
195
|
+
stateManager.messages_append(persona.id, eiMsg);
|
|
196
|
+
result.messagesImported++;
|
|
197
|
+
if (!isOld) toAnalyze.push(eiMsg);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
stateManager.messages_sort(persona.id);
|
|
201
|
+
eiInterface?.onMessageAdded?.(persona.id);
|
|
202
|
+
|
|
203
|
+
if (toAnalyze.length > 0 && !signal?.aborted) {
|
|
204
|
+
const allInState = stateManager.messages_get(persona.id);
|
|
205
|
+
const analyzeIds = new Set(toAnalyze.map((m) => m.id));
|
|
206
|
+
const analyzeStartIndex = allInState.findIndex((m) => analyzeIds.has(m.id));
|
|
207
|
+
const contextMsgs = analyzeStartIndex > 0 ? allInState.slice(0, analyzeStartIndex) : [];
|
|
208
|
+
|
|
209
|
+
const context: ExtractionContext = {
|
|
210
|
+
personaId: persona.id,
|
|
211
|
+
channelDisplayName: persona.display_name,
|
|
212
|
+
messages_context: contextMsgs,
|
|
213
|
+
messages_analyze: toAnalyze,
|
|
214
|
+
sources: [`pi:${getMachineId()}:${targetSession.id}`],
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
queuePersonRewritePhase(stateManager);
|
|
218
|
+
queueTopicRewritePhase(stateManager);
|
|
219
|
+
queueAllScans(context, stateManager, {
|
|
220
|
+
extraction_model: human.settings?.pi?.extraction_model,
|
|
221
|
+
external_filter: "only",
|
|
222
|
+
});
|
|
223
|
+
result.extractionScansQueued += 4;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
result.sessionsProcessed = 1;
|
|
227
|
+
updateProcessedState(stateManager, targetSession);
|
|
228
|
+
|
|
229
|
+
console.log(
|
|
230
|
+
`[Pi] Session complete: ${result.messagesImported} messages imported, ` +
|
|
231
|
+
`${result.extractionScansQueued} extraction scans queued`
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
return result;
|
|
235
|
+
}
|