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.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/src/cli/README.md +2 -0
  3. package/src/cli/install.ts +708 -0
  4. package/src/cli/retrieval.ts +22 -0
  5. package/src/cli/session-context.ts +98 -0
  6. package/src/cli.ts +2 -669
  7. package/src/core/bootstrap-tools.ts +486 -0
  8. package/src/core/handlers/document-segmentation.ts +1 -2
  9. package/src/core/handlers/heartbeat.ts +3 -2
  10. package/src/core/handlers/persona-response.ts +5 -4
  11. package/src/core/handlers/rooms.ts +6 -5
  12. package/src/core/integration-sync-manager.ts +482 -0
  13. package/src/core/message-manager.ts +2 -1
  14. package/src/core/migrations.ts +297 -0
  15. package/src/core/orchestrators/ceremony.ts +2 -1
  16. package/src/core/processor.ts +17 -1151
  17. package/src/core/room-manager.ts +17 -4
  18. package/src/core/state-manager.ts +2 -1
  19. package/src/core/types/entities.ts +1 -0
  20. package/src/core/utils/message-id.ts +15 -0
  21. package/src/integrations/claude-code/importer.ts +9 -30
  22. package/src/integrations/claude-code/types.ts +1 -1
  23. package/src/integrations/codex/importer.ts +6 -27
  24. package/src/integrations/codex/types.ts +1 -1
  25. package/src/integrations/constants.ts +3 -0
  26. package/src/integrations/cursor/importer.ts +9 -26
  27. package/src/integrations/cursor/types.ts +1 -1
  28. package/src/integrations/pi/importer.ts +235 -0
  29. package/src/integrations/pi/index.ts +3 -0
  30. package/src/integrations/pi/reader.ts +247 -0
  31. package/src/integrations/pi/types.ts +151 -0
  32. package/src/integrations/shared/message-converter.ts +41 -0
  33. package/src/integrations/slack/importer.ts +1 -1
  34. package/tui/README.md +1 -0
  35. package/tui/src/components/PromptInput.tsx +5 -1
  36. package/tui/src/util/yaml-settings.ts +28 -0
@@ -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 existing = sm.getRoomMessages(roomId).find(
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
- if (existing) {
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, ContextStatus, PersonaEntity, PersonaTrait } from "../../core/types.js";
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, ClaudeCodeMessage } from "./types.js";
5
- import {
6
- CLAUDE_CODE_PERSONA_NAME,
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
- function convertToEiMessage(msg: ClaudeCodeMessage, sessionId: string): Message {
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 const MIN_SESSION_AGE_MS = 20 * 60 * 1000;
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 { ContextStatus, Ei_Interface, Message, PersonaEntity, PersonaTrait } from "../../core/types.js";
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
- function convertToEiMessage(msg: CodexMessage, sessionId: string): Message {
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 const MIN_SESSION_AGE_MS = 20 * 60 * 1000;
97
+ export { MIN_SESSION_AGE_MS } from "../constants.js";
98
98
 
99
99
  // ============================================================================
100
100
  // Human Settings Shape
@@ -0,0 +1,3 @@
1
+ export const MIN_SESSION_AGE_MS = 20 * 60 * 1_000;
2
+
3
+ export const TWELVE_HOURS_MS = 43_200_000;
@@ -1,15 +1,14 @@
1
1
  import type { StateManager } from "../../core/state-manager.js";
2
- import type { Ei_Interface, Message, ContextStatus, PersonaEntity, PersonaTrait } from "../../core/types.js";
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
- CURSOR_PERSONA_NAME,
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
- function convertToEiMessage(msg: CursorMessage, sessionId: string): Message {
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 convertToPreMarkedEiMessage(msg: CursorMessage, sessionId: string): Message {
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 eiMsg = isOld ? convertToPreMarkedEiMessage(msg, targetSession.id) : convertToEiMessage(msg, targetSession.id);
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 const MIN_SESSION_AGE_MS = 20 * 60 * 1000;
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
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./types.js";
2
+ export * from "./reader.js";
3
+ export * from "./importer.js";