ei-tui 1.6.3 → 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.
@@ -0,0 +1,482 @@
1
+ import { StateManager } from "./state-manager.js";
2
+ import type { Ei_Interface, HumanEntity } from "./types.js";
3
+ import type { Storage } from "../storage/interface.js";
4
+
5
+ const DEFAULT_OPENCODE_POLLING_MS = 60000;
6
+ const DEFAULT_CLAUDE_CODE_POLLING_MS = 60000;
7
+ const DEFAULT_CURSOR_POLLING_MS = 60000;
8
+ const DEFAULT_CODEX_POLLING_MS = 60000;
9
+ const DEFAULT_PI_POLLING_MS = 60000;
10
+
11
+ export class IntegrationSyncManager {
12
+ private lastOpenCodeSync = 0;
13
+ private openCodeImportInProgress = false;
14
+ private lastClaudeCodeSync = 0;
15
+ private claudeCodeImportInProgress = false;
16
+ private lastCursorSync = 0;
17
+ private cursorImportInProgress = false;
18
+ private lastCodexSync = 0;
19
+ private codexImportInProgress = false;
20
+ private lastPiSync = 0;
21
+ private piImportInProgress = false;
22
+ private lastSlackSync = 0;
23
+ private slackImportInProgress = false;
24
+ private personaHistoryImportInProgress = false;
25
+
26
+ constructor(
27
+ private stateManager: StateManager,
28
+ private isTUI: boolean,
29
+ private storage: Storage | null,
30
+ private importAbortController: AbortController,
31
+ private ei: Ei_Interface,
32
+ ) {}
33
+
34
+ resetImportFlags(): void {
35
+ if (this.openCodeImportInProgress) {
36
+ console.log("[IntegrationSyncManager] Clearing openCodeImportInProgress flag");
37
+ this.openCodeImportInProgress = false;
38
+ }
39
+ if (this.claudeCodeImportInProgress) {
40
+ console.log("[IntegrationSyncManager] Clearing claudeCodeImportInProgress flag");
41
+ this.claudeCodeImportInProgress = false;
42
+ }
43
+ if (this.cursorImportInProgress) {
44
+ console.log("[IntegrationSyncManager] Clearing cursorImportInProgress flag");
45
+ this.cursorImportInProgress = false;
46
+ }
47
+ if (this.codexImportInProgress) {
48
+ console.log("[IntegrationSyncManager] Clearing codexImportInProgress flag");
49
+ this.codexImportInProgress = false;
50
+ }
51
+ if (this.piImportInProgress) {
52
+ console.log("[IntegrationSyncManager] Clearing piImportInProgress flag");
53
+ this.piImportInProgress = false;
54
+ }
55
+ if (this.slackImportInProgress) {
56
+ console.log("[IntegrationSyncManager] Clearing slackImportInProgress flag");
57
+ this.slackImportInProgress = false;
58
+ }
59
+ }
60
+
61
+ updateAbortController(controller: AbortController): void {
62
+ this.importAbortController = controller;
63
+ }
64
+
65
+ async checkAll(human: HumanEntity, now: number): Promise<void> {
66
+ if (
67
+ this.isTUI &&
68
+ human.settings?.opencode?.integration &&
69
+ this.stateManager.queue_length() === 0
70
+ ) {
71
+ await this.checkAndSyncOpenCode(human, now);
72
+ }
73
+
74
+ if (this.isTUI && human.settings?.backup?.enabled) {
75
+ await this.checkAndRunRollingBackup(human, now);
76
+ }
77
+
78
+ if (
79
+ this.isTUI &&
80
+ human.settings?.claudeCode?.integration &&
81
+ this.stateManager.queue_length() === 0
82
+ ) {
83
+ await this.checkAndSyncClaudeCode(human, now);
84
+ }
85
+
86
+ if (
87
+ this.isTUI &&
88
+ human.settings?.cursor?.integration &&
89
+ this.stateManager.queue_length() === 0
90
+ ) {
91
+ await this.checkAndSyncCursor(human, now);
92
+ }
93
+
94
+ if (
95
+ this.isTUI &&
96
+ human.settings?.codex?.integration &&
97
+ this.stateManager.queue_length() === 0
98
+ ) {
99
+ await this.checkAndSyncCodex(human, now);
100
+ }
101
+
102
+ if (
103
+ this.isTUI &&
104
+ human.settings?.pi?.integration &&
105
+ this.stateManager.queue_length() === 0
106
+ ) {
107
+ await this.checkAndSyncPi(human, now);
108
+ }
109
+
110
+ if (
111
+ this.isTUI &&
112
+ human.settings?.personaHistory?.integration &&
113
+ !human.settings.personaHistory.complete &&
114
+ this.stateManager.queue_length() === 0
115
+ ) {
116
+ await this.checkAndSyncPersonaHistory(human);
117
+ }
118
+
119
+ if (
120
+ this.isTUI &&
121
+ Object.values(human.settings?.slack?.workspaces ?? {}).some(ws => ws.integration && ws.auth) &&
122
+ this.stateManager.queue_length() === 0
123
+ ) {
124
+ await this.checkAndSyncSlack(human, now);
125
+ }
126
+ }
127
+
128
+ private async checkAndRunRollingBackup(human: HumanEntity, now: number): Promise<void> {
129
+ if (!this.storage) return;
130
+ const cfg = human.settings!.backup!;
131
+ const intervalMs = cfg.interval_ms ?? 3_600_000;
132
+ const maxBackups = cfg.max_backups ?? 24;
133
+ const lastBackup = cfg.last_backup ? new Date(cfg.last_backup).getTime() : 0;
134
+
135
+ if (now - lastBackup < intervalMs) return;
136
+
137
+ this.stateManager.setHuman({
138
+ ...this.stateManager.getHuman(),
139
+ settings: {
140
+ ...this.stateManager.getHuman().settings,
141
+ backup: { ...cfg, last_backup: new Date(now).toISOString() },
142
+ },
143
+ });
144
+
145
+ const state = this.stateManager.getStorageState();
146
+ try {
147
+ await this.storage.saveRollingBackup(state, maxBackups);
148
+ console.log(`[Processor] Rolling backup saved (max=${maxBackups})`);
149
+ } catch (err) {
150
+ console.warn(`[Processor] Rolling backup failed:`, err);
151
+ }
152
+ }
153
+
154
+ private async checkAndSyncOpenCode(human: HumanEntity, now: number): Promise<void> {
155
+ if (this.openCodeImportInProgress) {
156
+ return;
157
+ }
158
+
159
+ const opencode = human.settings?.opencode;
160
+ const pollingInterval = opencode?.polling_interval_ms ?? DEFAULT_OPENCODE_POLLING_MS;
161
+ const lastSync = opencode?.last_sync ? new Date(opencode.last_sync).getTime() : 0;
162
+ const timeSinceSync = now - lastSync;
163
+
164
+ if (timeSinceSync < pollingInterval && this.lastOpenCodeSync > 0) {
165
+ return;
166
+ }
167
+
168
+ this.lastOpenCodeSync = now;
169
+ const syncTimestamp = new Date().toISOString();
170
+ this.stateManager.setHuman({
171
+ ...this.stateManager.getHuman(),
172
+ settings: {
173
+ ...this.stateManager.getHuman().settings,
174
+ opencode: {
175
+ ...opencode,
176
+ last_sync: syncTimestamp,
177
+ },
178
+ },
179
+ });
180
+
181
+ this.openCodeImportInProgress = true;
182
+ import("../integrations/opencode/importer.js")
183
+ .then(({ importOpenCodeSessions }) =>
184
+ importOpenCodeSessions({
185
+ stateManager: this.stateManager,
186
+ interface: this.ei,
187
+ signal: this.importAbortController.signal,
188
+ })
189
+ )
190
+ .then((result) => {
191
+ if (result.sessionsProcessed > 0) {
192
+ console.log(
193
+ `[Processor] OpenCode sync complete: ${result.sessionsProcessed} sessions, ` +
194
+ `${result.messagesImported} messages imported, ` +
195
+ `${result.extractionScansQueued} extraction scans queued`
196
+ );
197
+ }
198
+ })
199
+ .catch((err) => {
200
+ console.warn(`[Processor] OpenCode sync failed:`, err);
201
+ })
202
+ .finally(() => {
203
+ this.openCodeImportInProgress = false;
204
+ });
205
+ }
206
+
207
+ private async checkAndSyncClaudeCode(human: HumanEntity, now: number): Promise<void> {
208
+ if (this.claudeCodeImportInProgress) {
209
+ return;
210
+ }
211
+
212
+ const claudeCode = human.settings?.claudeCode;
213
+ const pollingInterval = claudeCode?.polling_interval_ms ?? DEFAULT_CLAUDE_CODE_POLLING_MS;
214
+ const lastSync = claudeCode?.last_sync ? new Date(claudeCode.last_sync).getTime() : 0;
215
+ const timeSinceSync = now - lastSync;
216
+
217
+ if (timeSinceSync < pollingInterval && this.lastClaudeCodeSync > 0) {
218
+ return;
219
+ }
220
+
221
+ this.lastClaudeCodeSync = now;
222
+ const syncTimestamp = new Date().toISOString();
223
+ this.stateManager.setHuman({
224
+ ...this.stateManager.getHuman(),
225
+ settings: {
226
+ ...this.stateManager.getHuman().settings,
227
+ claudeCode: {
228
+ ...claudeCode,
229
+ last_sync: syncTimestamp,
230
+ },
231
+ },
232
+ });
233
+
234
+ this.claudeCodeImportInProgress = true;
235
+ import("../integrations/claude-code/importer.js")
236
+ .then(({ importClaudeCodeSessions }) =>
237
+ importClaudeCodeSessions({
238
+ stateManager: this.stateManager,
239
+ interface: this.ei,
240
+ signal: this.importAbortController.signal,
241
+ })
242
+ )
243
+ .then((result) => {
244
+ if (result.sessionsProcessed > 0) {
245
+ console.log(
246
+ `[Processor] Claude Code sync complete: ${result.sessionsProcessed} sessions, ` +
247
+ `${result.messagesImported} messages imported, ` +
248
+ `${result.extractionScansQueued} extraction scans queued`
249
+ );
250
+ }
251
+ })
252
+ .catch((err) => {
253
+ console.warn(`[Processor] Claude Code sync failed:`, err);
254
+ })
255
+ .finally(() => {
256
+ this.claudeCodeImportInProgress = false;
257
+ });
258
+ }
259
+
260
+ private async checkAndSyncCursor(human: HumanEntity, now: number): Promise<void> {
261
+ if (this.cursorImportInProgress) {
262
+ return;
263
+ }
264
+
265
+ const cursor = human.settings?.cursor;
266
+ const pollingInterval = cursor?.polling_interval_ms ?? DEFAULT_CURSOR_POLLING_MS;
267
+ const lastSync = cursor?.last_sync ? new Date(cursor.last_sync).getTime() : 0;
268
+ const timeSinceSync = now - lastSync;
269
+
270
+ if (timeSinceSync < pollingInterval && this.lastCursorSync > 0) {
271
+ return;
272
+ }
273
+
274
+ this.lastCursorSync = now;
275
+ const syncTimestamp = new Date().toISOString();
276
+ this.stateManager.setHuman({
277
+ ...this.stateManager.getHuman(),
278
+ settings: {
279
+ ...this.stateManager.getHuman().settings,
280
+ cursor: {
281
+ ...cursor,
282
+ last_sync: syncTimestamp,
283
+ },
284
+ },
285
+ });
286
+
287
+ this.cursorImportInProgress = true;
288
+ import("../integrations/cursor/importer.js")
289
+ .then(({ importCursorSessions }) =>
290
+ importCursorSessions({
291
+ stateManager: this.stateManager,
292
+ interface: this.ei,
293
+ signal: this.importAbortController.signal,
294
+ })
295
+ )
296
+ .then((result) => {
297
+ if (result.sessionsProcessed > 0) {
298
+ console.log(
299
+ `[Processor] Cursor sync complete: ${result.sessionsProcessed} sessions, ` +
300
+ `${result.messagesImported} messages imported, ` +
301
+ `${result.extractionScansQueued} extraction scans queued`
302
+ );
303
+ }
304
+ })
305
+ .catch((err) => {
306
+ console.warn(`[Processor] Cursor sync failed:`, err);
307
+ })
308
+ .finally(() => {
309
+ this.cursorImportInProgress = false;
310
+ });
311
+ }
312
+
313
+ private async checkAndSyncCodex(human: HumanEntity, now: number): Promise<void> {
314
+ if (this.codexImportInProgress) {
315
+ return;
316
+ }
317
+
318
+ const codex = human.settings?.codex;
319
+ const pollingInterval = codex?.polling_interval_ms ?? DEFAULT_CODEX_POLLING_MS;
320
+ const lastSync = codex?.last_sync ? new Date(codex.last_sync).getTime() : 0;
321
+ const timeSinceSync = now - lastSync;
322
+
323
+ if (timeSinceSync < pollingInterval && this.lastCodexSync > 0) {
324
+ return;
325
+ }
326
+
327
+ this.lastCodexSync = now;
328
+ const syncTimestamp = new Date().toISOString();
329
+ const currentHuman = this.stateManager.getHuman();
330
+ this.stateManager.setHuman({
331
+ ...currentHuman,
332
+ settings: {
333
+ ...currentHuman.settings,
334
+ codex: {
335
+ ...codex,
336
+ last_sync: syncTimestamp,
337
+ },
338
+ },
339
+ });
340
+
341
+ this.codexImportInProgress = true;
342
+ import("../integrations/codex/importer.js")
343
+ .then(({ importCodexSessions }) =>
344
+ importCodexSessions({
345
+ stateManager: this.stateManager,
346
+ interface: this.ei,
347
+ signal: this.importAbortController.signal,
348
+ })
349
+ )
350
+ .then((result) => {
351
+ if (result.sessionsProcessed > 0) {
352
+ console.log(
353
+ `[Processor] Codex sync complete: ${result.sessionsProcessed} sessions, ` +
354
+ `${result.messagesImported} messages imported, ` +
355
+ `${result.extractionScansQueued} extraction scans queued`
356
+ );
357
+ }
358
+ })
359
+ .catch((err) => {
360
+ console.warn(`[Processor] Codex sync failed:`, err);
361
+ })
362
+ .finally(() => {
363
+ this.codexImportInProgress = false;
364
+ });
365
+ }
366
+
367
+ private async checkAndSyncPi(human: HumanEntity, now: number): Promise<void> {
368
+ if (this.piImportInProgress) {
369
+ return;
370
+ }
371
+
372
+ const pi = human.settings?.pi;
373
+ const pollingInterval = pi?.polling_interval_ms ?? DEFAULT_PI_POLLING_MS;
374
+ const lastSync = pi?.last_sync ? new Date(pi.last_sync).getTime() : 0;
375
+ const timeSinceSync = now - lastSync;
376
+
377
+ if (timeSinceSync < pollingInterval && this.lastPiSync > 0) {
378
+ return;
379
+ }
380
+
381
+ this.lastPiSync = now;
382
+ const syncTimestamp = new Date().toISOString();
383
+ const currentHuman = this.stateManager.getHuman();
384
+ this.stateManager.setHuman({
385
+ ...currentHuman,
386
+ settings: {
387
+ ...currentHuman.settings,
388
+ pi: {
389
+ ...pi,
390
+ last_sync: syncTimestamp,
391
+ },
392
+ },
393
+ });
394
+
395
+ this.piImportInProgress = true;
396
+ import("../integrations/pi/importer.js")
397
+ .then(({ importPiSessions }) =>
398
+ importPiSessions({
399
+ stateManager: this.stateManager,
400
+ interface: this.ei,
401
+ signal: this.importAbortController.signal,
402
+ })
403
+ )
404
+ .then((result) => {
405
+ if (result.sessionsProcessed > 0) {
406
+ console.log(
407
+ `[Processor] Pi sync complete: ${result.sessionsProcessed} sessions, ` +
408
+ `${result.messagesImported} messages imported, ` +
409
+ `${result.extractionScansQueued} extraction scans queued`
410
+ );
411
+ }
412
+ })
413
+ .catch((err) => {
414
+ console.warn(`[Processor] Pi sync failed:`, err);
415
+ })
416
+ .finally(() => {
417
+ this.piImportInProgress = false;
418
+ });
419
+ }
420
+
421
+ private async checkAndSyncSlack(human: HumanEntity, now: number): Promise<void> {
422
+ if (this.slackImportInProgress) return;
423
+
424
+ const slack = human.settings?.slack;
425
+ const pollingInterval = slack?.polling_interval_ms ?? 60_000;
426
+
427
+ if (now - this.lastSlackSync < pollingInterval && this.lastSlackSync > 0) return;
428
+
429
+ this.lastSlackSync = now;
430
+
431
+ this.slackImportInProgress = true;
432
+ import("../integrations/slack/importer.js")
433
+ .then(({ importSlackChannel }) =>
434
+ importSlackChannel({
435
+ stateManager: this.stateManager,
436
+ interface: this.ei,
437
+ signal: this.importAbortController.signal,
438
+ })
439
+ )
440
+ .then((result) => {
441
+ if (result.channelProcessed) {
442
+ console.log(
443
+ `[Processor] Slack sync: #${result.channelProcessed} — ` +
444
+ `${result.messagesImported} messages, ${result.threadsProcessed} threads, ` +
445
+ `${result.scansQueued} scans queued`
446
+ );
447
+ }
448
+ })
449
+ .catch((err) => {
450
+ const msg = err instanceof Error ? err.message : JSON.stringify(err);
451
+ const stack = err instanceof Error ? err.stack : undefined;
452
+ console.warn(`[Processor] Slack sync failed: ${msg}${stack ? `\n${stack}` : ''}`);
453
+ })
454
+ .finally(() => {
455
+ this.slackImportInProgress = false;
456
+ });
457
+ }
458
+
459
+ private async checkAndSyncPersonaHistory(_human: HumanEntity): Promise<void> {
460
+ if (this.personaHistoryImportInProgress) return;
461
+
462
+ this.personaHistoryImportInProgress = true;
463
+ import("../integrations/persona-history/importer.js")
464
+ .then(({ importPersonaHistory }) =>
465
+ importPersonaHistory({ stateManager: this.stateManager })
466
+ )
467
+ .then((result) => {
468
+ if (result.scansQueued > 0) {
469
+ console.log(
470
+ `[Processor] PersonaHistory: ${result.scansQueued} scans queued` +
471
+ (result.complete ? " — import complete" : "")
472
+ );
473
+ }
474
+ })
475
+ .catch((err) => {
476
+ console.warn(`[Processor] PersonaHistory sync failed:`, err);
477
+ })
478
+ .finally(() => {
479
+ this.personaHistoryImportInProgress = false;
480
+ });
481
+ }
482
+ }
@@ -25,6 +25,7 @@ import {
25
25
  } from "./orchestrators/index.js";
26
26
  import { buildChatMessageContent } from "../prompts/message-utils.js";
27
27
  import { filterMessagesForContext } from "./context-utils.js";
28
+ import { qualifyEiMessage } from "./utils/message-id.js";
28
29
 
29
30
  // =============================================================================
30
31
  // MESSAGE QUERIES
@@ -149,7 +150,7 @@ export async function sendMessage(
149
150
  clearPendingRequestsFor(sm, qp, currentRequest, personaId);
150
151
 
151
152
  const message: Message = {
152
- id: crypto.randomUUID(),
153
+ id: qualifyEiMessage(crypto.randomUUID()),
153
154
  role: "human",
154
155
  content: content ?? undefined,
155
156
  silence_reason: content ? undefined : (silenceReason ?? "passed"),