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.
@@ -35,7 +35,11 @@ import { QueueProcessor } from "./queue-processor.js";
35
35
  import { handlers } from "./handlers/index.js";
36
36
  import { normalizeRoomMessages, getMessageContent } from "./handlers/utils.js";
37
37
  import { sanitizeEiPersonaIdentifiers } from "./utils/identifier-utils.js";
38
+ import { qualifyEiMessage } from "./utils/message-id.js";
38
39
  import { ContextStatus as ContextStatusEnum, RoomMode } from "./types.js";
40
+ import { bootstrapTools } from "./bootstrap-tools.js";
41
+ import { seedBuiltinFacts, migrateLearnedOn, migrateMessageIds, migrateSlackToMultiWorkspace, seedSettings } from "./migrations.js";
42
+ import { IntegrationSyncManager } from "./integration-sync-manager.js";
39
43
  import { registerFindMemoryExecutor, registerFetchMemoryExecutor, registerFetchMessageExecutor, registerFileReadExecutor, registerPersonaNoteExecutors, buildPersonaNoteTools, SYSTEM_TOOLS } from "./tools/index.js";
40
44
  import { createAddNoteExecutor, createClearNoteExecutor } from "./tools/builtin/persona-notes.js";
41
45
  import { createFindMemoryExecutor } from "./tools/builtin/find-memory.js";
@@ -46,7 +50,6 @@ import { EMMETT_PERSONA_DEFINITION } from "../templates/emmett.js";
46
50
  import { shouldStartCeremony, startCeremony, handleCeremonyProgress, queueReflectionDrain, queueUserDedupRequest, queueRoomCapture, queuePersonaCapture, checkAndQueueRoomExtraction, queueTargetedPersonUpdate, queueTargetedTopicUpdate } from "./orchestrators/index.js";
47
51
  import { finishDocumentBatch } from "./handlers/document-segmentation.js";
48
52
  import { buildSynthesisPrompt } from "../prompts/synthesis/index.js";
49
- import { BUILT_IN_FACTS } from "./constants/built-in-facts.js";
50
53
  import { DEFAULT_SEED_TRAITS } from "./constants/seed-traits.js";
51
54
 
52
55
  // Static module imports
@@ -140,16 +143,8 @@ import {
140
143
  import type { RoomCreationInput, RoomEntity, RoomMessage, RoomSummary } from "./types.js";
141
144
  import { previewUnsource as _previewUnsource } from "../integrations/document/unsource.js";
142
145
  import type { UnsourcePreview, UnsourceResult } from "../integrations/document/unsource.js";
143
- import { isQualifiedMessageId, qualifyEiMessage, qualifyOpenCodeMessage } from "./utils/message-id.js";
144
-
145
- import type { IOpenCodeReader } from "../integrations/opencode/types.js";
146
146
 
147
147
  const DEFAULT_LOOP_INTERVAL_MS = 100;
148
- const DEFAULT_OPENCODE_POLLING_MS = 60000;
149
- const DEFAULT_CLAUDE_CODE_POLLING_MS = 60000;
150
- const DEFAULT_CURSOR_POLLING_MS = 60000;
151
- const DEFAULT_CODEX_POLLING_MS = 60000;
152
- const DEFAULT_PI_POLLING_MS = 60000;
153
148
 
154
149
  let processorInstanceCount = 0;
155
150
 
@@ -165,22 +160,11 @@ export class Processor {
165
160
  private instanceId: number;
166
161
  private currentRequest: LLMRequest | null = null;
167
162
  private isTUI = false;
168
- private lastOpenCodeSync = 0;
169
163
  private lastDLQTrim = 0;
170
- private openCodeImportInProgress = false;
171
- private lastClaudeCodeSync = 0;
172
- private claudeCodeImportInProgress = false;
173
- private lastCursorSync = 0;
174
- private cursorImportInProgress = false;
175
- private lastCodexSync = 0;
176
- private codexImportInProgress = false;
177
- private lastPiSync = 0;
178
- private piImportInProgress = false;
179
- private lastSlackSync = 0;
180
- private slackImportInProgress = false;
181
164
  private pendingConflict: StateConflictData | null = null;
182
165
  private storage: Storage | null = null;
183
166
  private importAbortController = new AbortController();
167
+ private syncManager: IntegrationSyncManager | null = null;
184
168
  private personaPreviewResolvers = new Map<string, { resolve: (r: PersonaGenerationResult) => void; reject: (e: Error) => void }>();
185
169
 
186
170
  constructor(ei: Ei_Interface) {
@@ -252,11 +236,11 @@ export class Processor {
252
236
  await this.bootstrapFirstRun();
253
237
  }
254
238
  this.bootstrapTools();
255
- this.seedBuiltinFacts();
256
- this.migrateLearnedOn();
257
- await this.migrateMessageIds();
258
- this.migrateSlackToMultiWorkspace();
259
- this.seedSettings();
239
+ seedBuiltinFacts(this.stateManager);
240
+ migrateLearnedOn(this.stateManager);
241
+ await migrateMessageIds(this.stateManager, this.isTUI);
242
+ migrateSlackToMultiWorkspace(this.stateManager);
243
+ seedSettings(this.stateManager);
260
244
  registerFindMemoryExecutor(createFindMemoryExecutor(this.searchHumanData.bind(this), this.getPersonaList.bind(this), this.stateManager.getHuman.bind(this.stateManager)));
261
245
  registerFetchMemoryExecutor(createFetchMemoryExecutor(this.stateManager.getHuman.bind(this.stateManager)));
262
246
  registerPersonaNoteExecutors(
@@ -284,6 +268,7 @@ export class Processor {
284
268
  (roomId: string) => this.stateManager.getRoom(roomId)?.display_name ?? null
285
269
  ));
286
270
  }
271
+ this.syncManager = new IntegrationSyncManager(this.stateManager, this.isTUI, this.storage, this.importAbortController, this.interface);
287
272
  this.running = true;
288
273
  console.log(`[Processor ${this.instanceId}] initialized, starting loop`);
289
274
  this.runLoop();
@@ -313,7 +298,7 @@ export class Processor {
313
298
  this.stateManager.persona_add(eiEntity);
314
299
 
315
300
  const welcomeMessage: Message = {
316
- id: crypto.randomUUID(),
301
+ id: qualifyEiMessage(crypto.randomUUID()),
317
302
  role: "system",
318
303
  content: EI_WELCOME_MESSAGE,
319
304
  timestamp: new Date().toISOString(),
@@ -499,761 +484,8 @@ export class Processor {
499
484
  return this.generateDocument(subject);
500
485
  }
501
486
 
502
- /**
503
- * Seed built-in tool providers and tools if they don't exist yet.
504
- * Called on every startup (after state load/restore) — safe to call repeatedly.
505
- * New builtins added in future releases will be seeded automatically.
506
- */
507
487
  private bootstrapTools(): void {
508
- const now = new Date().toISOString();
509
-
510
- for (const name of ["find_memory", "fetch_memory", "fetch_message", "read_memory"]) {
511
- const tool = this.stateManager.tools_getByName(name);
512
- if (tool) this.stateManager.tools_remove(tool.id);
513
- }
514
-
515
- // --- Ei built-in provider ---
516
- if (!this.stateManager.tools_getProviderById("ei")) {
517
- const eiProvider: ToolProvider = {
518
- id: "ei",
519
- name: "ei",
520
- display_name: "Ei Built-ins",
521
- description: "Built-in tools that ship with Ei. No external API needed.",
522
- builtin: true,
523
- config: {},
524
- enabled: true,
525
- created_at: now,
526
- };
527
- this.stateManager.tools_addProvider(eiProvider);
528
- }
529
-
530
- // file_read tool (TUI only)
531
- this.stateManager.tools_upsertBuiltin({
532
- id: crypto.randomUUID(),
533
- provider_id: "ei",
534
- name: "file_read",
535
- display_name: "Read File",
536
- description:
537
- "Read the contents of a file from the local filesystem. Use list_directory first to explore folder structure. Only available in the TUI.",
538
- input_schema: {
539
- type: "object",
540
- properties: {
541
- path: { type: "string", description: "Absolute or relative path to the file" },
542
- },
543
- required: ["path"],
544
- },
545
- runtime: "node",
546
- builtin: true,
547
- enabled: true,
548
- created_at: now,
549
- max_calls_per_interaction: 5,
550
- });
551
-
552
- // list_directory tool (TUI only)
553
- this.stateManager.tools_upsertBuiltin({
554
- id: crypto.randomUUID(),
555
- provider_id: "ei",
556
- name: "list_directory",
557
- display_name: "List Directory",
558
- description:
559
- "List the contents of a directory on the local filesystem. Returns filenames prefixed with [FILE] or [DIR]. Use this to explore folder structure before reading files. Only available in the TUI.",
560
- input_schema: {
561
- type: "object",
562
- properties: {
563
- path: { type: "string", description: "Absolute or relative path to the directory (e.g. ~/Projects/myapp or /home/user/docs)" },
564
- },
565
- required: ["path"],
566
- },
567
- runtime: "node",
568
- builtin: true,
569
- enabled: true,
570
- created_at: now,
571
- max_calls_per_interaction: 5,
572
- });
573
-
574
- // directory_tree tool (TUI only)
575
- this.stateManager.tools_upsertBuiltin({
576
- id: crypto.randomUUID(),
577
- provider_id: "ei",
578
- name: "directory_tree",
579
- display_name: "Directory Tree",
580
- description:
581
- "Show a recursive tree of a directory up to a configurable depth. Returns a JSON tree with name, type, and children fields. Default max_depth is 3, maximum is 8. Only available in the TUI.",
582
- input_schema: {
583
- type: "object",
584
- properties: {
585
- path: { type: "string", description: "Absolute or relative path to the directory" },
586
- max_depth: { type: "number", description: "Maximum depth to recurse (1-8, default 3)" },
587
- },
588
- required: ["path"],
589
- },
590
- runtime: "node",
591
- builtin: true,
592
- enabled: true,
593
- created_at: now,
594
- max_calls_per_interaction: 3,
595
- });
596
-
597
- // search_files tool (TUI only)
598
- this.stateManager.tools_upsertBuiltin({
599
- id: crypto.randomUUID(),
600
- provider_id: "ei",
601
- name: "search_files",
602
- display_name: "Search Files",
603
- description:
604
- "Recursively search for files by name pattern within a directory. Supports * wildcards. Returns matching absolute paths. Skips node_modules, .git, dist. Only available in the TUI.",
605
- input_schema: {
606
- type: "object",
607
- properties: {
608
- path: { type: "string", description: "Root directory to search from" },
609
- pattern: { type: "string", description: "Filename glob pattern, e.g. \"*.ts\" or \"README*\"" },
610
- },
611
- required: ["path", "pattern"],
612
- },
613
- runtime: "node",
614
- builtin: true,
615
- enabled: true,
616
- created_at: now,
617
- max_calls_per_interaction: 3,
618
- });
619
-
620
- // grep tool (TUI only)
621
- this.stateManager.tools_upsertBuiltin({
622
- id: crypto.randomUUID(),
623
- provider_id: "ei",
624
- name: "grep",
625
- display_name: "Grep",
626
- description:
627
- "Search file contents for lines matching a regex pattern. Recursively searches a directory (or a single file). Skips binary files and node_modules. Returns matching file, line number, and text. Only available in the TUI.",
628
- input_schema: {
629
- type: "object",
630
- properties: {
631
- pattern: { type: "string", description: "Regex pattern to search for" },
632
- path: { type: "string", description: "File or directory to search" },
633
- include: { type: "string", description: "Optional glob to filter filenames, e.g. \"*.ts\"" },
634
- case_insensitive: { type: "boolean", description: "Case-insensitive match (default false)" },
635
- },
636
- required: ["pattern", "path"],
637
- },
638
- runtime: "node",
639
- builtin: true,
640
- enabled: true,
641
- created_at: now,
642
- max_calls_per_interaction: 5,
643
- });
644
-
645
- // get_file_info tool (TUI only)
646
- this.stateManager.tools_upsertBuiltin({
647
- id: crypto.randomUUID(),
648
- provider_id: "ei",
649
- name: "get_file_info",
650
- display_name: "Get File Info",
651
- description:
652
- "Get metadata about a file or directory: type, size, permissions, created/modified/accessed timestamps. Only available in the TUI.",
653
- input_schema: {
654
- type: "object",
655
- properties: {
656
- path: { type: "string", description: "Absolute or relative path to the file or directory" },
657
- },
658
- required: ["path"],
659
- },
660
- runtime: "node",
661
- builtin: true,
662
- enabled: true,
663
- created_at: now,
664
- max_calls_per_interaction: 5,
665
- });
666
-
667
- // web_fetch tool
668
- this.stateManager.tools_upsertBuiltin({
669
- id: crypto.randomUUID(),
670
- provider_id: "ei",
671
- name: "web_fetch",
672
- display_name: "Web Fetch",
673
- description:
674
- "Fetch content from a URL and return the text. Useful for reading web pages, documentation, or public APIs. HTML is stripped to plain text. Only available in the TUI.",
675
- input_schema: {
676
- type: "object",
677
- properties: {
678
- url: { type: "string", description: "The URL to fetch (http or https only)" },
679
- },
680
- required: ["url"],
681
- },
682
- runtime: "node",
683
- builtin: true,
684
- enabled: true,
685
- created_at: now,
686
- max_calls_per_interaction: 3,
687
- });
688
-
689
- // --- Tavily Search provider ---
690
- if (!this.stateManager.tools_getProviderById("tavily")) {
691
- const tavilyProvider: ToolProvider = {
692
- id: "tavily",
693
- name: "tavily",
694
- display_name: "Tavily Search",
695
- description:
696
- "Browser-compatible web search. Requires a Tavily API key (free tier: 1000 requests/month).",
697
- builtin: true,
698
- config: { api_key: "" },
699
- enabled: false,
700
- created_at: now,
701
- };
702
- this.stateManager.tools_addProvider(tavilyProvider);
703
- }
704
-
705
- // tavily_web_search
706
- this.stateManager.tools_upsertBuiltin({
707
- id: crypto.randomUUID(),
708
- provider_id: "tavily",
709
- name: "tavily_web_search",
710
- display_name: "Web Search",
711
- description:
712
- "Search the web using Tavily. Use for current events, fact verification, or any topic that benefits from up-to-date information.",
713
- input_schema: {
714
- type: "object",
715
- properties: {
716
- query: { type: "string", description: "Search query" },
717
- max_results: { type: "number", description: "Number of results (default: 5, max: 10)" },
718
- },
719
- required: ["query"],
720
- },
721
- runtime: "any",
722
- builtin: true,
723
- enabled: true,
724
- created_at: now,
725
- max_calls_per_interaction: 3,
726
- });
727
-
728
- // tavily_news_search
729
- this.stateManager.tools_upsertBuiltin({
730
- id: crypto.randomUUID(),
731
- provider_id: "tavily",
732
- name: "tavily_news_search",
733
- display_name: "News Search",
734
- description:
735
- "Search recent news articles using Tavily. Use for current events and recent developments.",
736
- input_schema: {
737
- type: "object",
738
- properties: {
739
- query: { type: "string", description: "News search query" },
740
- max_results: { type: "number", description: "Number of results (default: 5, max: 10)" },
741
- },
742
- required: ["query"],
743
- },
744
- runtime: "any",
745
- builtin: true,
746
- enabled: true,
747
- created_at: now,
748
- max_calls_per_interaction: 3,
749
- });
750
-
751
- // --- Spotify provider ---
752
- if (!this.stateManager.tools_getProviderById("spotify")) {
753
- const spotifyProvider: ToolProvider = {
754
- id: "spotify",
755
- name: "spotify",
756
- display_name: "Spotify",
757
- description:
758
- "Access your Spotify playback and music library. Connect via Settings → Tool Kits → Spotify.",
759
- builtin: true,
760
- config: { spotify_refresh_token: "" },
761
- enabled: false,
762
- created_at: now,
763
- };
764
- this.stateManager.tools_addProvider(spotifyProvider);
765
- }
766
-
767
- // get_currently_playing
768
- this.stateManager.tools_upsertBuiltin({
769
- id: crypto.randomUUID(),
770
- provider_id: "spotify",
771
- name: "get_currently_playing",
772
- display_name: "Currently Playing",
773
- description:
774
- "Get the song currently playing on the user's Spotify. Returns artist, title, album, playback state, and progress. Returns nothing_playing if nothing is active.",
775
- input_schema: {
776
- type: "object",
777
- properties: {},
778
- required: [],
779
- },
780
- runtime: "any",
781
- builtin: true,
782
- enabled: true,
783
- created_at: now,
784
- max_calls_per_interaction: 3,
785
- });
786
-
787
- // get_liked_songs
788
- this.stateManager.tools_upsertBuiltin({
789
- id: crypto.randomUUID(),
790
- provider_id: "spotify",
791
- name: "get_liked_songs",
792
- display_name: "Liked Songs",
793
- description:
794
- "Get the user's full Spotify liked songs library. Returns an array of { artist, title, added_at }. Results are cached for 30 minutes. Ask the user before calling — it may return thousands of tracks.",
795
- input_schema: {
796
- type: "object",
797
- properties: {},
798
- required: [],
799
- },
800
- runtime: "any",
801
- builtin: true,
802
- enabled: true,
803
- created_at: now,
804
- max_calls_per_interaction: 1,
805
- });
806
-
807
- // submit_response tool — auto-injected for Heartbeat steps only (HandleHeartbeatCheck).
808
- // PersonaResponse and RoomResponse agents now use natural Markdown output instead.
809
- // Not user-configurable; invisible in the tools UI. Terminates the tool loop immediately
810
- // when called; its arguments become response.parsed.
811
- this.stateManager.tools_upsertBuiltin({
812
- id: crypto.randomUUID(),
813
- provider_id: "ei",
814
- name: "submit_response",
815
- display_name: "Submit Response",
816
- description: "Submit your response to the conversation. Call this when you are ready to respond — after any research or tool use is complete.",
817
- input_schema: {
818
- type: "object",
819
- properties: {
820
- should_respond: {
821
- type: "boolean",
822
- description: "Whether you are responding (true) or staying silent (false)",
823
- },
824
- content: {
825
- type: "string",
826
- description: "Your response in Markdown. Required when should_respond is true. Use _underscores_ for actions or stage directions inline with your text.",
827
- },
828
- reason: {
829
- type: "string",
830
- description: "Why you are staying silent. Only used when should_respond is false.",
831
- },
832
- },
833
- required: ["should_respond"],
834
- additionalProperties: false,
835
- },
836
- runtime: "any",
837
- builtin: true,
838
- enabled: true,
839
- is_submit: true,
840
- max_calls_per_interaction: 1,
841
- created_at: now,
842
- });
843
-
844
- this.stateManager.tools_upsertBuiltin({
845
- id: crypto.randomUUID(),
846
- provider_id: "ei",
847
- name: "submit_heartbeat_check",
848
- display_name: "Submit Heartbeat Decision",
849
- description: "Submit your decision on whether to reach out with a message. Call this when you have decided.",
850
- input_schema: {
851
- type: "object",
852
- properties: {
853
- should_respond: { type: "boolean", description: "Whether you want to initiate a message" },
854
- topic: { type: "string", description: "The specific topic you want to discuss (when should_respond is true)" },
855
- message: { type: "string", description: "Your actual message to them (when should_respond is true)" },
856
- },
857
- required: ["should_respond"],
858
- additionalProperties: false,
859
- },
860
- runtime: "any",
861
- builtin: true,
862
- enabled: true,
863
- is_submit: true,
864
- max_calls_per_interaction: 1,
865
- created_at: now,
866
- });
867
-
868
- this.stateManager.tools_upsertBuiltin({
869
- id: crypto.randomUUID(),
870
- provider_id: "ei",
871
- name: "submit_ei_heartbeat",
872
- display_name: "Submit Ei Heartbeat Decision",
873
- description: "Submit your choice of item to follow up on, or indicate nothing warrants reaching out.",
874
- input_schema: {
875
- type: "object",
876
- properties: {
877
- should_respond: { type: "boolean", description: "Whether Ei wants to check in about an item" },
878
- id: { type: "string", description: "ID of the item you chose (when should_respond is true)" },
879
- my_response: { type: "string", description: "The check-in message (for Person/Topic/Persona items)" },
880
- },
881
- required: ["should_respond"],
882
- additionalProperties: false,
883
- },
884
- runtime: "any",
885
- builtin: true,
886
- enabled: true,
887
- is_submit: true,
888
- max_calls_per_interaction: 1,
889
- created_at: now,
890
- });
891
-
892
- this.stateManager.tools_upsertBuiltin({
893
- id: crypto.randomUUID(),
894
- provider_id: "ei",
895
- name: "submit_dedup_decisions",
896
- display_name: "Submit Dedup Decisions",
897
- description: "Submit your merge, remove, and add decisions for this cluster of records.",
898
- input_schema: {
899
- type: "object",
900
- properties: {
901
- update: {
902
- type: "array",
903
- description: "Records to update with merged data. Must include at least one (the canonical record).",
904
- items: {
905
- type: "object",
906
- properties: {
907
- id: { type: "string" },
908
- type: { type: "string", enum: ["topic", "person", "trait"] },
909
- name: { type: "string" },
910
- description: { type: "string" },
911
- sentiment: { type: "number" },
912
- strength: { type: "number" },
913
- confidence: { type: "number" },
914
- exposure_current: { type: "number" },
915
- exposure_desired: { type: "number" },
916
- relationship: { type: "string" },
917
- category: { type: "string" },
918
- last_updated: { type: "string" },
919
- },
920
- required: ["id", "type", "name", "description"],
921
- additionalProperties: false,
922
- },
923
- },
924
- remove: {
925
- type: "array",
926
- description: "Duplicates to remove. Each must reference its canonical record via replaced_by.",
927
- items: {
928
- type: "object",
929
- properties: {
930
- to_be_removed: { type: "string" },
931
- replaced_by: { type: "string" },
932
- },
933
- required: ["to_be_removed", "replaced_by"],
934
- additionalProperties: false,
935
- },
936
- },
937
- add: {
938
- type: "array",
939
- description: "New records to create. Only when merging reveals a missing concept.",
940
- items: {
941
- type: "object",
942
- properties: {
943
- type: { type: "string", enum: ["topic", "person", "trait"] },
944
- name: { type: "string" },
945
- description: { type: "string" },
946
- sentiment: { type: "number" },
947
- strength: { type: "number" },
948
- confidence: { type: "number" },
949
- exposure_current: { type: "number" },
950
- exposure_desired: { type: "number" },
951
- relationship: { type: "string" },
952
- category: { type: "string" },
953
- },
954
- required: ["type", "name", "description"],
955
- additionalProperties: false,
956
- },
957
- },
958
- },
959
- required: ["update", "remove", "add"],
960
- additionalProperties: false,
961
- },
962
- runtime: "any",
963
- builtin: true,
964
- enabled: true,
965
- is_submit: true,
966
- max_calls_per_interaction: 1,
967
- created_at: now,
968
- });
969
-
970
- // --- Reconcile pass: prune stale tool references from persona tool lists ---
971
- // Build manifest of all tool IDs currently in state (everything seeded above).
972
- const manifestIds = new Set(this.stateManager.tools_getAll().map(t => t.id));
973
-
974
- for (const persona of this.stateManager.persona_getAll()) {
975
- if (!persona.tools?.length) continue;
976
- const pruned = persona.tools.filter(id => manifestIds.has(id));
977
- if (pruned.length !== persona.tools.length) {
978
- const removed = persona.tools.length - pruned.length;
979
- this.stateManager.persona_update(persona.id, { tools: pruned });
980
- console.log(`[Processor] Pruned ${removed} stale tool reference(s) from persona "${persona.display_name}"`);
981
- }
982
- }
983
- }
984
-
985
- /**
986
- * Seed 25 built-in facts if they don't exist yet.
987
- * Called on every startup — safe to call repeatedly.
988
- * New facts are created with empty descriptions and validated_date.
989
- */
990
- private seedBuiltinFacts(): void {
991
- const human = this.stateManager.getHuman();
992
- const existingFactNames = new Set(human.facts.map(f => f.name));
993
-
994
- // BUILT_IN_FACTS imported at top of file
995
- const now = new Date().toISOString();
996
- let seededCount = 0;
997
-
998
- for (const builtInFact of BUILT_IN_FACTS) {
999
- if (existingFactNames.has(builtInFact.name)) continue;
1000
-
1001
- const newFact: Fact = {
1002
- id: crypto.randomUUID(),
1003
- name: builtInFact.name,
1004
- description: '',
1005
- sentiment: 0,
1006
- validated_date: '',
1007
- last_updated: now,
1008
- learned_on: now,
1009
- };
1010
- human.facts.push(newFact);
1011
- seededCount++;
1012
- }
1013
-
1014
- if (seededCount > 0) {
1015
- this.stateManager.setHuman(human);
1016
- console.log(`[Processor] Seeded ${seededCount} built-in facts`);
1017
- }
1018
- }
1019
-
1020
- private migrateLearnedOn(): void {
1021
- const human = this.stateManager.getHuman();
1022
-
1023
- const backfill = <T extends { learned_on?: string; last_updated: string }>(items: T[]): T[] =>
1024
- items.map(item => item.learned_on ? item : { ...item, learned_on: item.last_updated });
1025
-
1026
- const facts = backfill(human.facts);
1027
- const topics = backfill(human.topics);
1028
- const people = backfill(human.people);
1029
-
1030
- const changed =
1031
- facts.some((f, i) => f !== human.facts[i]) ||
1032
- topics.some((t, i) => t !== human.topics[i]) ||
1033
- people.some((p, i) => p !== human.people[i]);
1034
-
1035
- if (changed) {
1036
- this.stateManager.setHuman({ ...human, facts, topics, people });
1037
- console.log("[Processor] Backfilled learned_on for existing data items");
1038
- }
1039
- }
1040
-
1041
- private async migrateMessageIds(): Promise<void> {
1042
- try {
1043
- let msgRewrites = 0;
1044
- let quoteRewrites = 0;
1045
-
1046
- const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1047
-
1048
- const personas = this.stateManager.persona_getAll();
1049
- for (const persona of personas) {
1050
- for (const msg of this.stateManager.messages_get(persona.id)) {
1051
- if (!msg.external && UUID_PATTERN.test(msg.id)) {
1052
- this.stateManager.messages_update(persona.id, msg.id, { id: qualifyEiMessage(msg.id) });
1053
- msgRewrites++;
1054
- }
1055
- }
1056
- }
1057
-
1058
- const rooms = this.stateManager.getRoomList();
1059
- for (const room of rooms) {
1060
- for (const msg of this.stateManager.getRoomMessages(room.id).slice()) {
1061
- if (UUID_PATTERN.test(msg.id)) {
1062
- this.stateManager.updateRoomMessage(room.id, msg.id, { id: qualifyEiMessage(msg.id) });
1063
- msgRewrites++;
1064
- }
1065
- }
1066
- }
1067
-
1068
- const human = this.stateManager.getHuman();
1069
- const quotes = human.quotes ?? [];
1070
-
1071
- const eiUuidMap = new Map<string, string>();
1072
- for (const persona of personas) {
1073
- for (const msg of this.stateManager.messages_get(persona.id)) {
1074
- if (msg.id.startsWith("ei:")) eiUuidMap.set(msg.id.slice(3), msg.id);
1075
- }
1076
- }
1077
- for (const room of rooms) {
1078
- for (const msg of this.stateManager.getRoomMessages(room.id)) {
1079
- if (msg.id.startsWith("ei:")) eiUuidMap.set(msg.id.slice(3), msg.id);
1080
- }
1081
- }
1082
-
1083
- const MSG_PATTERN = /^msg_[a-zA-Z0-9]+$/;
1084
-
1085
- let openCodeReader: IOpenCodeReader | null = null;
1086
- if (this.isTUI) {
1087
- const { createOpenCodeReader } = await import("../integrations/opencode/reader-factory.js");
1088
- openCodeReader = await createOpenCodeReader().catch(() => null);
1089
- }
1090
-
1091
- const updatedQuotes: typeof quotes = [];
1092
- for (const quote of quotes) {
1093
- const mid = quote.message_id;
1094
- if (!mid || isQualifiedMessageId(mid)) {
1095
- updatedQuotes.push(quote);
1096
- continue;
1097
- }
1098
-
1099
- if (MSG_PATTERN.test(mid)) {
1100
- if (openCodeReader) {
1101
- const ocWindow = await openCodeReader.getMessageById(mid).catch(() => null);
1102
- if (ocWindow) {
1103
- const { getMachineId } = await import("../integrations/machine-id.js");
1104
- updatedQuotes.push({ ...quote, message_id: qualifyOpenCodeMessage(getMachineId(), ocWindow.session.id, mid) });
1105
- quoteRewrites++;
1106
- continue;
1107
- }
1108
- }
1109
- updatedQuotes.push(quote);
1110
- continue;
1111
- }
1112
-
1113
- if (UUID_PATTERN.test(mid)) {
1114
- const fqId = eiUuidMap.get(mid);
1115
- if (fqId) {
1116
- updatedQuotes.push({ ...quote, message_id: fqId });
1117
- quoteRewrites++;
1118
- continue;
1119
- }
1120
- updatedQuotes.push(quote);
1121
- continue;
1122
- }
1123
-
1124
- updatedQuotes.push(quote);
1125
- }
1126
-
1127
- if (quoteRewrites > 0) {
1128
- this.stateManager.setHuman({ ...human, quotes: updatedQuotes });
1129
- }
1130
-
1131
- if (msgRewrites > 0 || quoteRewrites > 0) {
1132
- console.log(`[Processor] migrateMessageIds: rewrote ${msgRewrites} message IDs, ${quoteRewrites} quote message_ids`);
1133
- }
1134
- } catch (err) {
1135
- console.error("[Processor] migrateMessageIds failed, continuing:", err);
1136
- }
1137
- }
1138
-
1139
- private migrateSlackToMultiWorkspace(): void {
1140
- const human = this.stateManager.getHuman();
1141
- const slack = human.settings?.slack as Record<string, unknown> | undefined;
1142
- if (!slack) return;
1143
-
1144
- const hasLegacyAuth = "auth" in slack && slack.auth != null;
1145
- const hasLegacyIntegration = "integration" in slack;
1146
- if (!hasLegacyAuth && !hasLegacyIntegration) return;
1147
-
1148
- const legacyAuth = slack.auth as Record<string, unknown> | undefined;
1149
- const workspaceId = (legacyAuth?.workspace_id as string | undefined) ?? "unknown";
1150
-
1151
- const migratedWorkspace: Record<string, unknown> = {
1152
- integration: slack.integration,
1153
- extraction_model: slack.extraction_model,
1154
- last_sync: slack.last_sync,
1155
- backfill_days: slack.backfill_days,
1156
- broadcast_threshold: slack.broadcast_threshold,
1157
- channel_overrides: slack.channel_overrides,
1158
- channels: slack.channels,
1159
- };
1160
-
1161
- if (legacyAuth) {
1162
- migratedWorkspace.auth = {
1163
- type: "oauth",
1164
- token: legacyAuth.token,
1165
- refresh_token: legacyAuth.refresh_token,
1166
- workspace_name: legacyAuth.workspace_name,
1167
- };
1168
- }
1169
-
1170
- this.stateManager.setHuman({
1171
- ...human,
1172
- settings: {
1173
- ...human.settings,
1174
- slack: {
1175
- polling_interval_ms: slack.polling_interval_ms as number | undefined,
1176
- workspaces: { [workspaceId]: migratedWorkspace } as unknown as import("../integrations/slack/types.js").SlackSettings["workspaces"],
1177
- },
1178
- },
1179
- });
1180
-
1181
- console.log(`[Processor] migrateSlackToMultiWorkspace: migrated legacy slack settings to workspaces[${workspaceId}]`);
1182
- }
1183
-
1184
- private seedSettings(): void {
1185
- const human = this.stateManager.getHuman();
1186
- let modified = false;
1187
-
1188
- if (!human.settings) {
1189
- human.settings = {};
1190
- modified = true;
1191
- }
1192
-
1193
- if (!human.settings.opencode) {
1194
- human.settings.opencode = {
1195
- integration: false,
1196
- polling_interval_ms: 60000,
1197
- };
1198
- modified = true;
1199
- }
1200
-
1201
- if (!human.settings.claudeCode) {
1202
- human.settings.claudeCode = {
1203
- integration: false,
1204
- polling_interval_ms: 60000,
1205
- };
1206
- modified = true;
1207
- }
1208
-
1209
- if (!human.settings.codex) {
1210
- human.settings.codex = {
1211
- integration: false,
1212
- polling_interval_ms: 60000,
1213
- };
1214
- modified = true;
1215
- }
1216
-
1217
- if (!human.settings.ceremony) {
1218
- human.settings.ceremony = {
1219
- time: "09:00",
1220
- };
1221
- modified = true;
1222
- }
1223
-
1224
- if (!human.settings.backup) {
1225
- human.settings.backup = {
1226
- enabled: false,
1227
- max_backups: 24,
1228
- interval_ms: 3600000,
1229
- };
1230
- modified = true;
1231
- }
1232
-
1233
- if (human.settings.default_heartbeat_ms == null) {
1234
- human.settings.default_heartbeat_ms = 1800000;
1235
- modified = true;
1236
- }
1237
-
1238
- if (human.settings.default_context_window_ms == null) {
1239
- human.settings.default_context_window_ms = 28800000;
1240
- modified = true;
1241
- }
1242
-
1243
- if (human.settings.message_min_count == null) {
1244
- human.settings.message_min_count = 0;
1245
- modified = true;
1246
- }
1247
-
1248
- if (human.settings.message_max_age_days == null) {
1249
- human.settings.message_max_age_days = 0;
1250
- modified = true;
1251
- }
1252
-
1253
- if (modified) {
1254
- this.stateManager.setHuman(human);
1255
- console.log(`[Processor] Seeded missing settings`);
1256
- }
488
+ bootstrapTools(this.stateManager);
1257
489
  }
1258
490
 
1259
491
  async stop(): Promise<void> {
@@ -1283,30 +515,7 @@ export class Processor {
1283
515
  this.running = false;
1284
516
  this.queueProcessor.abort();
1285
517
  this.importAbortController.abort();
1286
- if (this.openCodeImportInProgress) {
1287
- console.log(`[Processor ${this.instanceId}] Clearing openCodeImportInProgress flag`);
1288
- this.openCodeImportInProgress = false;
1289
- }
1290
- if (this.claudeCodeImportInProgress) {
1291
- console.log(`[Processor ${this.instanceId}] Clearing claudeCodeImportInProgress flag`);
1292
- this.claudeCodeImportInProgress = false;
1293
- }
1294
- if (this.cursorImportInProgress) {
1295
- console.log(`[Processor ${this.instanceId}] Clearing cursorImportInProgress flag`);
1296
- this.cursorImportInProgress = false;
1297
- }
1298
- if (this.codexImportInProgress) {
1299
- console.log(`[Processor ${this.instanceId}] Clearing codexImportInProgress flag`);
1300
- this.codexImportInProgress = false;
1301
- }
1302
- if (this.piImportInProgress) {
1303
- console.log(`[Processor ${this.instanceId}] Clearing piImportInProgress flag`);
1304
- this.piImportInProgress = false;
1305
- }
1306
- if (this.slackImportInProgress) {
1307
- console.log(`[Processor ${this.instanceId}] Clearing slackImportInProgress flag`);
1308
- this.slackImportInProgress = false;
1309
- }
518
+ this.syncManager?.resetImportFlags();
1310
519
  await this.stateManager.flush();
1311
520
  console.log(`[Processor ${this.instanceId}] pause() complete (main loop stopped, state flushed)`);
1312
521
  }
@@ -1317,6 +526,7 @@ export class Processor {
1317
526
  throw new Error(`Cannot resume a stopped processor (instanceId: ${this.instanceId})`);
1318
527
  }
1319
528
  this.importAbortController = new AbortController();
529
+ this.syncManager?.updateAbortController(this.importAbortController);
1320
530
  this.running = true;
1321
531
  this.runLoop();
1322
532
  console.log(`[Processor ${this.instanceId}] resume() complete (main loop restarted)`);
@@ -1521,64 +731,8 @@ const toolNextSteps = new Set([
1521
731
 
1522
732
  const human = this.stateManager.getHuman();
1523
733
 
1524
- if (
1525
- this.isTUI &&
1526
- human.settings?.opencode?.integration &&
1527
- this.stateManager.queue_length() === 0
1528
- ) {
1529
- await this.checkAndSyncOpenCode(human, now);
1530
- }
1531
-
1532
- if (this.isTUI && human.settings?.backup?.enabled) {
1533
- await this.checkAndRunRollingBackup(human, now);
1534
- }
1535
- if (
1536
- this.isTUI &&
1537
- human.settings?.claudeCode?.integration &&
1538
- this.stateManager.queue_length() === 0
1539
- ) {
1540
- await this.checkAndSyncClaudeCode(human, now);
1541
- }
1542
-
1543
- if (
1544
- this.isTUI &&
1545
- human.settings?.cursor?.integration &&
1546
- this.stateManager.queue_length() === 0
1547
- ) {
1548
- await this.checkAndSyncCursor(human, now);
1549
- }
1550
-
1551
- if (
1552
- this.isTUI &&
1553
- human.settings?.codex?.integration &&
1554
- this.stateManager.queue_length() === 0
1555
- ) {
1556
- await this.checkAndSyncCodex(human, now);
1557
- }
1558
-
1559
- if (
1560
- this.isTUI &&
1561
- human.settings?.pi?.integration &&
1562
- this.stateManager.queue_length() === 0
1563
- ) {
1564
- await this.checkAndSyncPi(human, now);
1565
- }
1566
-
1567
- if (
1568
- this.isTUI &&
1569
- human.settings?.personaHistory?.integration &&
1570
- !human.settings.personaHistory.complete &&
1571
- this.stateManager.queue_length() === 0
1572
- ) {
1573
- await this.checkAndSyncPersonaHistory(human);
1574
- }
1575
-
1576
- if (
1577
- this.isTUI &&
1578
- Object.values(human.settings?.slack?.workspaces ?? {}).some(ws => ws.integration && ws.auth) &&
1579
- this.stateManager.queue_length() === 0
1580
- ) {
1581
- await this.checkAndSyncSlack(human, now);
734
+ if (this.syncManager) {
735
+ await this.syncManager.checkAll(human, now);
1582
736
  }
1583
737
 
1584
738
  if (human.settings?.ceremony && shouldStartCeremony(human.settings.ceremony, this.stateManager)) {
@@ -1636,363 +790,6 @@ const toolNextSteps = new Set([
1636
790
  }
1637
791
  }
1638
792
 
1639
- private async checkAndRunRollingBackup(human: HumanEntity, now: number): Promise<void> {
1640
- if (!this.storage) return;
1641
- const cfg = human.settings!.backup!;
1642
- const intervalMs = cfg.interval_ms ?? 3_600_000;
1643
- const maxBackups = cfg.max_backups ?? 24;
1644
- const lastBackup = cfg.last_backup ? new Date(cfg.last_backup).getTime() : 0;
1645
-
1646
- if (now - lastBackup < intervalMs) return;
1647
-
1648
- this.stateManager.setHuman({
1649
- ...this.stateManager.getHuman(),
1650
- settings: {
1651
- ...this.stateManager.getHuman().settings,
1652
- backup: { ...cfg, last_backup: new Date(now).toISOString() },
1653
- },
1654
- });
1655
-
1656
- const state = this.stateManager.getStorageState();
1657
- try {
1658
- await this.storage.saveRollingBackup(state, maxBackups);
1659
- console.log(`[Processor] Rolling backup saved (max=${maxBackups})`);
1660
- } catch (err) {
1661
- console.warn(`[Processor] Rolling backup failed:`, err);
1662
- }
1663
- }
1664
-
1665
- private async checkAndSyncOpenCode(human: HumanEntity, now: number): Promise<void> {
1666
- if (this.openCodeImportInProgress) {
1667
- return;
1668
- }
1669
-
1670
- const opencode = human.settings?.opencode;
1671
- const pollingInterval = opencode?.polling_interval_ms ?? DEFAULT_OPENCODE_POLLING_MS;
1672
- const lastSync = opencode?.last_sync ? new Date(opencode.last_sync).getTime() : 0;
1673
- const timeSinceSync = now - lastSync;
1674
-
1675
- if (timeSinceSync < pollingInterval && this.lastOpenCodeSync > 0) {
1676
- return;
1677
- }
1678
-
1679
- this.lastOpenCodeSync = now;
1680
- const syncTimestamp = new Date().toISOString();
1681
- this.stateManager.setHuman({
1682
- ...this.stateManager.getHuman(),
1683
- settings: {
1684
- ...this.stateManager.getHuman().settings,
1685
- opencode: {
1686
- ...opencode,
1687
- last_sync: syncTimestamp,
1688
- },
1689
- },
1690
- });
1691
-
1692
- this.openCodeImportInProgress = true;
1693
- import("../integrations/opencode/importer.js")
1694
- .then(({ importOpenCodeSessions }) =>
1695
- importOpenCodeSessions({
1696
- stateManager: this.stateManager,
1697
- interface: this.interface,
1698
- signal: this.importAbortController.signal,
1699
- })
1700
- )
1701
- .then((result) => {
1702
- if (result.sessionsProcessed > 0) {
1703
- console.log(
1704
- `[Processor] OpenCode sync complete: ${result.sessionsProcessed} sessions, ` +
1705
- `${result.messagesImported} messages imported, ` +
1706
- `${result.extractionScansQueued} extraction scans queued`
1707
- );
1708
- }
1709
- })
1710
- .catch((err) => {
1711
- console.warn(`[Processor] OpenCode sync failed:`, err);
1712
- })
1713
- .finally(() => {
1714
- this.openCodeImportInProgress = false;
1715
- });
1716
- }
1717
-
1718
- private async checkAndSyncClaudeCode(human: HumanEntity, now: number): Promise<void> {
1719
- if (this.claudeCodeImportInProgress) {
1720
- return;
1721
- }
1722
-
1723
- const claudeCode = human.settings?.claudeCode;
1724
- const pollingInterval = claudeCode?.polling_interval_ms ?? DEFAULT_CLAUDE_CODE_POLLING_MS;
1725
- const lastSync = claudeCode?.last_sync ? new Date(claudeCode.last_sync).getTime() : 0;
1726
- const timeSinceSync = now - lastSync;
1727
-
1728
- if (timeSinceSync < pollingInterval && this.lastClaudeCodeSync > 0) {
1729
- return;
1730
- }
1731
-
1732
- this.lastClaudeCodeSync = now;
1733
- const syncTimestamp = new Date().toISOString();
1734
- this.stateManager.setHuman({
1735
- ...this.stateManager.getHuman(),
1736
- settings: {
1737
- ...this.stateManager.getHuman().settings,
1738
- claudeCode: {
1739
- ...claudeCode,
1740
- last_sync: syncTimestamp,
1741
- },
1742
- },
1743
- });
1744
-
1745
- this.claudeCodeImportInProgress = true;
1746
- import("../integrations/claude-code/importer.js")
1747
- .then(({ importClaudeCodeSessions }) =>
1748
- importClaudeCodeSessions({
1749
- stateManager: this.stateManager,
1750
- interface: this.interface,
1751
- signal: this.importAbortController.signal,
1752
- })
1753
- )
1754
- .then((result) => {
1755
- if (result.sessionsProcessed > 0) {
1756
- console.log(
1757
- `[Processor] Claude Code sync complete: ${result.sessionsProcessed} sessions, ` +
1758
- `${result.messagesImported} messages imported, ` +
1759
- `${result.extractionScansQueued} extraction scans queued`
1760
- );
1761
- }
1762
- })
1763
- .catch((err) => {
1764
- console.warn(`[Processor] Claude Code sync failed:`, err);
1765
- })
1766
- .finally(() => {
1767
- this.claudeCodeImportInProgress = false;
1768
- });
1769
- }
1770
-
1771
- private async checkAndSyncCursor(human: HumanEntity, now: number): Promise<void> {
1772
- if (this.cursorImportInProgress) {
1773
- return;
1774
- }
1775
-
1776
- const cursor = human.settings?.cursor;
1777
- const pollingInterval = cursor?.polling_interval_ms ?? DEFAULT_CURSOR_POLLING_MS;
1778
- const lastSync = cursor?.last_sync ? new Date(cursor.last_sync).getTime() : 0;
1779
- const timeSinceSync = now - lastSync;
1780
-
1781
- if (timeSinceSync < pollingInterval && this.lastCursorSync > 0) {
1782
- return;
1783
- }
1784
-
1785
- this.lastCursorSync = now;
1786
- const syncTimestamp = new Date().toISOString();
1787
- this.stateManager.setHuman({
1788
- ...this.stateManager.getHuman(),
1789
- settings: {
1790
- ...this.stateManager.getHuman().settings,
1791
- cursor: {
1792
- ...cursor,
1793
- last_sync: syncTimestamp,
1794
- },
1795
- },
1796
- });
1797
-
1798
- this.cursorImportInProgress = true;
1799
- import("../integrations/cursor/importer.js")
1800
- .then(({ importCursorSessions }) =>
1801
- importCursorSessions({
1802
- stateManager: this.stateManager,
1803
- interface: this.interface,
1804
- signal: this.importAbortController.signal,
1805
- })
1806
- )
1807
- .then((result) => {
1808
- if (result.sessionsProcessed > 0) {
1809
- console.log(
1810
- `[Processor] Cursor sync complete: ${result.sessionsProcessed} sessions, ` +
1811
- `${result.messagesImported} messages imported, ` +
1812
- `${result.extractionScansQueued} extraction scans queued`
1813
- );
1814
- }
1815
- })
1816
- .catch((err) => {
1817
- console.warn(`[Processor] Cursor sync failed:`, err);
1818
- })
1819
- .finally(() => {
1820
- this.cursorImportInProgress = false;
1821
- });
1822
- }
1823
-
1824
- private async checkAndSyncCodex(human: HumanEntity, now: number): Promise<void> {
1825
- if (this.codexImportInProgress) {
1826
- return;
1827
- }
1828
-
1829
- const codex = human.settings?.codex;
1830
- const pollingInterval = codex?.polling_interval_ms ?? DEFAULT_CODEX_POLLING_MS;
1831
- const lastSync = codex?.last_sync ? new Date(codex.last_sync).getTime() : 0;
1832
- const timeSinceSync = now - lastSync;
1833
-
1834
- if (timeSinceSync < pollingInterval && this.lastCodexSync > 0) {
1835
- return;
1836
- }
1837
-
1838
- this.lastCodexSync = now;
1839
- const syncTimestamp = new Date().toISOString();
1840
- const currentHuman = this.stateManager.getHuman();
1841
- this.stateManager.setHuman({
1842
- ...currentHuman,
1843
- settings: {
1844
- ...currentHuman.settings,
1845
- codex: {
1846
- ...codex,
1847
- last_sync: syncTimestamp,
1848
- },
1849
- },
1850
- });
1851
-
1852
- this.codexImportInProgress = true;
1853
- import("../integrations/codex/importer.js")
1854
- .then(({ importCodexSessions }) =>
1855
- importCodexSessions({
1856
- stateManager: this.stateManager,
1857
- interface: this.interface,
1858
- signal: this.importAbortController.signal,
1859
- })
1860
- )
1861
- .then((result) => {
1862
- if (result.sessionsProcessed > 0) {
1863
- console.log(
1864
- `[Processor] Codex sync complete: ${result.sessionsProcessed} sessions, ` +
1865
- `${result.messagesImported} messages imported, ` +
1866
- `${result.extractionScansQueued} extraction scans queued`
1867
- );
1868
- }
1869
- })
1870
- .catch((err) => {
1871
- console.warn(`[Processor] Codex sync failed:`, err);
1872
- })
1873
- .finally(() => {
1874
- this.codexImportInProgress = false;
1875
- });
1876
- }
1877
-
1878
- private async checkAndSyncPi(human: HumanEntity, now: number): Promise<void> {
1879
- if (this.piImportInProgress) {
1880
- return;
1881
- }
1882
-
1883
- const pi = human.settings?.pi;
1884
- const pollingInterval = pi?.polling_interval_ms ?? DEFAULT_PI_POLLING_MS;
1885
- const lastSync = pi?.last_sync ? new Date(pi.last_sync).getTime() : 0;
1886
- const timeSinceSync = now - lastSync;
1887
-
1888
- if (timeSinceSync < pollingInterval && this.lastPiSync > 0) {
1889
- return;
1890
- }
1891
-
1892
- this.lastPiSync = now;
1893
- const syncTimestamp = new Date().toISOString();
1894
- const currentHuman = this.stateManager.getHuman();
1895
- this.stateManager.setHuman({
1896
- ...currentHuman,
1897
- settings: {
1898
- ...currentHuman.settings,
1899
- pi: {
1900
- ...pi,
1901
- last_sync: syncTimestamp,
1902
- },
1903
- },
1904
- });
1905
-
1906
- this.piImportInProgress = true;
1907
- import("../integrations/pi/importer.js")
1908
- .then(({ importPiSessions }) =>
1909
- importPiSessions({
1910
- stateManager: this.stateManager,
1911
- interface: this.interface,
1912
- signal: this.importAbortController.signal,
1913
- })
1914
- )
1915
- .then((result) => {
1916
- if (result.sessionsProcessed > 0) {
1917
- console.log(
1918
- `[Processor] Pi sync complete: ${result.sessionsProcessed} sessions, ` +
1919
- `${result.messagesImported} messages imported, ` +
1920
- `${result.extractionScansQueued} extraction scans queued`
1921
- );
1922
- }
1923
- })
1924
- .catch((err) => {
1925
- console.warn(`[Processor] Pi sync failed:`, err);
1926
- })
1927
- .finally(() => {
1928
- this.piImportInProgress = false;
1929
- });
1930
- }
1931
-
1932
- private async checkAndSyncSlack(human: HumanEntity, now: number): Promise<void> {
1933
- if (this.slackImportInProgress) return;
1934
-
1935
- const slack = human.settings?.slack;
1936
- const pollingInterval = slack?.polling_interval_ms ?? 60_000;
1937
-
1938
- if (now - this.lastSlackSync < pollingInterval && this.lastSlackSync > 0) return;
1939
-
1940
- this.lastSlackSync = now;
1941
-
1942
- this.slackImportInProgress = true;
1943
- import("../integrations/slack/importer.js")
1944
- .then(({ importSlackChannel }) =>
1945
- importSlackChannel({
1946
- stateManager: this.stateManager,
1947
- interface: this.interface,
1948
- signal: this.importAbortController.signal,
1949
- })
1950
- )
1951
- .then((result) => {
1952
- if (result.channelProcessed) {
1953
- console.log(
1954
- `[Processor] Slack sync: #${result.channelProcessed} — ` +
1955
- `${result.messagesImported} messages, ${result.threadsProcessed} threads, ` +
1956
- `${result.scansQueued} scans queued`
1957
- );
1958
- }
1959
- })
1960
- .catch((err) => {
1961
- const msg = err instanceof Error ? err.message : JSON.stringify(err);
1962
- const stack = err instanceof Error ? err.stack : undefined;
1963
- console.warn(`[Processor] Slack sync failed: ${msg}${stack ? `\n${stack}` : ''}`);
1964
- })
1965
- .finally(() => {
1966
- this.slackImportInProgress = false;
1967
- });
1968
- }
1969
-
1970
- private personaHistoryImportInProgress = false;
1971
-
1972
- private async checkAndSyncPersonaHistory(_human: HumanEntity): Promise<void> {
1973
- if (this.personaHistoryImportInProgress) return;
1974
-
1975
- this.personaHistoryImportInProgress = true;
1976
- import("../integrations/persona-history/importer.js")
1977
- .then(({ importPersonaHistory }) =>
1978
- importPersonaHistory({ stateManager: this.stateManager })
1979
- )
1980
- .then((result) => {
1981
- if (result.scansQueued > 0) {
1982
- console.log(
1983
- `[Processor] PersonaHistory: ${result.scansQueued} scans queued` +
1984
- (result.complete ? " — import complete" : "")
1985
- );
1986
- }
1987
- })
1988
- .catch((err) => {
1989
- console.warn(`[Processor] PersonaHistory sync failed:`, err);
1990
- })
1991
- .finally(() => {
1992
- this.personaHistoryImportInProgress = false;
1993
- });
1994
- }
1995
-
1996
793
  private augmentRoomRequest(request: LLMRequest): LLMRequest {
1997
794
  if (request.next_step !== LLMNextStep.HandleRoomResponse) return request;
1998
795