@siftd/connect-agent 0.2.37 → 0.2.38

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.
@@ -11,9 +11,11 @@ import { AdvancedMemoryStore } from './core/memory-advanced.js';
11
11
  import { PostgresMemoryStore, isPostgresConfigured } from './core/memory-postgres.js';
12
12
  import { TaskScheduler } from './core/scheduler.js';
13
13
  import { SystemIndexer } from './core/system-indexer.js';
14
+ import { ContextGraph, ContextGraphKinds } from './core/context-graph.js';
14
15
  import { BashTool } from './tools/bash.js';
15
16
  import { WebTools } from './tools/web.js';
16
17
  import { WorkerTools } from './tools/worker.js';
18
+ import { CalendarTools } from './tools/calendar.js';
17
19
  import { SharedState } from './workers/shared-state.js';
18
20
  import { getKnowledgeForPrompt } from './genesis/index.js';
19
21
  import { loadHubContext, formatHubContext, logAction, logWorker } from './core/hub.js';
@@ -90,11 +92,21 @@ TOOL RULES:
90
92
  - Building, testing, deploying
91
93
  - Any filesystem modification
92
94
 
95
+ ATTACHMENTS:
96
+ - If the user message includes "📎 Attached files:", include those URLs verbatim in any worker task.
97
+ - Workers should download the files from the URLs and proceed without asking for the content again.
98
+
93
99
  MEMORY:
94
100
  - Search memory before starting work (context is valuable)
95
101
  - Remember important things after significant actions
96
102
  - Learn from failures - what went wrong, how to avoid it
97
103
 
104
+ CONTEXT GRAPH:
105
+ - Use remember_entity to store people, teams, projects, tools, tasks
106
+ - Use remember_relation to link entities (owns, works_on, depends_on, etc.)
107
+ - Use search_context to retrieve team context before asking users to repeat
108
+ - If user asks for "/context", call search_context and summarize results
109
+
98
110
  YOUR HUB: ~/Lia-Hub/
99
111
  - **CLAUDE.md** - User's instructions (always follow these)
100
112
  - **LANDMARKS.md** - Current state and context
@@ -109,22 +121,52 @@ The Lia interface has a built-in gallery that shows worker outputs automatically
109
121
  ✅ Workers write to ~/Lia-Hub/shared/outputs/
110
122
  ✅ Gallery displays assets in-app automatically
111
123
 
124
+ FILES BROWSER:
125
+ Users can type /files to open the Finder UI (cloud mode).
126
+ When asked to browse or locate files, point them to /files.
127
+ Refer to /files instead of internal Lia-Hub paths in responses.
128
+
129
+ FILES SCOPES:
130
+ - "My Files" are private to the user (default /files view)
131
+ - "Team Files" are shared across the org (use /files team or the Team Files location)
132
+ - If a user asks to share with the team, store outputs in Team Files and mention that it is shared
133
+
134
+ CALENDAR + TODO (Lia-managed data):
135
+ - /cal reads ~/Lia-Hub/shared/outputs/.lia/calendar.json
136
+ - /todo reads ~/Lia-Hub/shared/outputs/.lia/todos.json
137
+ - When users add, edit, or delete events, use calendar_upsert_events
138
+ - When users add, edit, or delete tasks, use todo_upsert_items
139
+ - Keep these files hidden; refer users to /cal or /todo in responses
140
+ - Schemas:
141
+ - calendar.json: { "version": 1, "calendars": [...], "events": [...] }
142
+ - todos.json: { "version": 1, "items": [...] }
143
+
112
144
  WORKFLOW:
113
145
  Before complex work: Check CLAUDE.md → Read LANDMARKS.md → Search memory
114
146
  After completing work: Update LANDMARKS.md → Remember learnings
115
147
 
116
- You orchestrate through workers. You remember through memory. You never do file operations directly.`;
148
+ You orchestrate through workers. You remember through memory. You never do arbitrary file operations directly (calendar_upsert_events and todo_upsert_items are the safe exceptions).`;
117
149
  export class MasterOrchestrator {
118
150
  client;
119
151
  model;
120
152
  maxTokens;
121
153
  memory;
154
+ contextGraph;
155
+ orgMemory;
156
+ orgContextGraph;
157
+ orgId;
158
+ orgRole;
159
+ instanceMode;
160
+ userOrgMemories;
161
+ userOrgContextGraphs;
122
162
  scheduler;
123
163
  indexer;
124
164
  jobs = new Map();
125
165
  jobCounter = 0;
126
166
  workerStatusCallback = null;
167
+ workerLogCallback = null;
127
168
  workerStatusInterval = null;
169
+ attachmentContext = null;
128
170
  galleryCallback = null;
129
171
  userId;
130
172
  workspaceDir;
@@ -134,6 +176,7 @@ export class MasterOrchestrator {
134
176
  bashTool;
135
177
  webTools;
136
178
  workerTools;
179
+ calendarTools;
137
180
  sharedState;
138
181
  verboseMode = new Map(); // per-user verbose mode
139
182
  constructor(options) {
@@ -141,10 +184,16 @@ export class MasterOrchestrator {
141
184
  this.model = options.model || 'claude-sonnet-4-20250514';
142
185
  this.maxTokens = options.maxTokens || 4096;
143
186
  this.userId = options.userId;
187
+ this.orgId = options.orgId;
188
+ this.orgRole = options.orgRole;
189
+ this.instanceMode = options.instanceMode || 'personal';
144
190
  this.workspaceDir = options.workspaceDir || process.env.HOME || '/tmp';
191
+ this.userOrgMemories = new Map();
192
+ this.userOrgContextGraphs = new Map();
145
193
  // Find claude binary - critical for worker delegation
146
194
  this.claudePath = this.findClaudeBinary();
147
195
  console.log(`[ORCHESTRATOR] Claude binary: ${this.claudePath}`);
196
+ console.log(`[ORCHESTRATOR] Instance mode: ${this.instanceMode}`);
148
197
  // Initialize memory - use Postgres if DATABASE_URL is set, otherwise SQLite
149
198
  if (isPostgresConfigured()) {
150
199
  console.log('[ORCHESTRATOR] DATABASE_URL detected, using PostgreSQL + pgvector');
@@ -160,6 +209,44 @@ export class MasterOrchestrator {
160
209
  voyageApiKey: options.voyageApiKey
161
210
  });
162
211
  }
212
+ this.contextGraph = new ContextGraph(this.memory, { scope: options.userId });
213
+ // Initialize org memory for team mode or when orgId is provided
214
+ if (this.orgId) {
215
+ if (isPostgresConfigured()) {
216
+ this.orgMemory = new PostgresMemoryStore({
217
+ connectionString: process.env.DATABASE_URL,
218
+ userId: `org:${this.orgId}`
219
+ });
220
+ }
221
+ else {
222
+ this.orgMemory = new AdvancedMemoryStore({
223
+ dbPath: `memories_org_${this.orgId}.db`,
224
+ vectorPath: `./memory_vectors_org_${this.orgId}`,
225
+ voyageApiKey: options.voyageApiKey
226
+ });
227
+ }
228
+ this.orgContextGraph = new ContextGraph(this.orgMemory, { scope: `org:${this.orgId}` });
229
+ }
230
+ // For personal mode: initialize memory stores for all user's teams (for reading)
231
+ if (this.instanceMode === 'personal' && options.userOrgIds) {
232
+ for (const orgId of options.userOrgIds) {
233
+ if (isPostgresConfigured()) {
234
+ this.userOrgMemories.set(orgId, new PostgresMemoryStore({
235
+ connectionString: process.env.DATABASE_URL,
236
+ userId: `org:${orgId}`
237
+ }));
238
+ }
239
+ else {
240
+ this.userOrgMemories.set(orgId, new AdvancedMemoryStore({
241
+ dbPath: `memories_org_${orgId}.db`,
242
+ vectorPath: `./memory_vectors_org_${orgId}`,
243
+ voyageApiKey: options.voyageApiKey
244
+ }));
245
+ }
246
+ this.userOrgContextGraphs.set(orgId, new ContextGraph(this.userOrgMemories.get(orgId), { scope: `org:${orgId}` }));
247
+ }
248
+ console.log(`[ORCHESTRATOR] Personal mode: initialized ${this.userOrgMemories.size} team memory stores for reading`);
249
+ }
163
250
  this.scheduler = new TaskScheduler(`scheduled_${options.userId}.json`);
164
251
  // Create indexer (will index on first initialize call)
165
252
  this.indexer = new SystemIndexer(this.memory);
@@ -167,7 +254,13 @@ export class MasterOrchestrator {
167
254
  this.bashTool = new BashTool(this.workspaceDir);
168
255
  this.webTools = new WebTools();
169
256
  this.workerTools = new WorkerTools(this.workspaceDir);
257
+ this.calendarTools = new CalendarTools();
170
258
  this.sharedState = new SharedState(this.workspaceDir);
259
+ this.workerTools.setLogCallback((workerId, line, stream) => {
260
+ if (this.workerLogCallback) {
261
+ this.workerLogCallback(workerId, line, stream);
262
+ }
263
+ });
171
264
  }
172
265
  /**
173
266
  * Initialize the orchestrator - indexes filesystem on first call
@@ -208,6 +301,12 @@ export class MasterOrchestrator {
208
301
  }
209
302
  }
210
303
  }
304
+ /**
305
+ * Set callback for worker log streaming
306
+ */
307
+ setWorkerLogCallback(callback) {
308
+ this.workerLogCallback = callback;
309
+ }
211
310
  /**
212
311
  * Set callback for gallery updates (worker assets for UI gallery view)
213
312
  */
@@ -412,6 +511,35 @@ export class MasterOrchestrator {
412
511
  }
413
512
  return null;
414
513
  }
514
+ extractAttachmentContext(message) {
515
+ const marker = '📎 Attached files:';
516
+ const index = message.indexOf(marker);
517
+ if (index === -1)
518
+ return null;
519
+ const raw = message.slice(index + marker.length);
520
+ const lines = raw
521
+ .split('\n')
522
+ .map(line => line.trim())
523
+ .filter(Boolean);
524
+ if (lines.length === 0)
525
+ return null;
526
+ return lines.join('\n');
527
+ }
528
+ withAttachments(task, context) {
529
+ if (!this.attachmentContext)
530
+ return { task, context };
531
+ const attachmentBlock = this.attachmentContext;
532
+ const hasAttachments = task.includes(attachmentBlock) ||
533
+ task.includes('Attached files') ||
534
+ (context && (context.includes(attachmentBlock) || context.includes('Attached files')));
535
+ if (hasAttachments)
536
+ return { task, context };
537
+ const attachmentNote = `Attached files (download these URLs and use their contents; do not ask the user to resend):\n${attachmentBlock}`;
538
+ if (context && context.trim().length > 0) {
539
+ return { task, context: `${context}\n\n${attachmentNote}` };
540
+ }
541
+ return { task: `${task}\n\n${attachmentNote}` };
542
+ }
415
543
  /**
416
544
  * Check if verbose mode is enabled
417
545
  */
@@ -428,9 +556,15 @@ export class MasterOrchestrator {
428
556
  if (slashResponse) {
429
557
  return slashResponse;
430
558
  }
559
+ // Deterministic calendar/todo writes (do not rely on the model calling tools)
560
+ const quickWrite = this.tryHandleCalendarTodo(message);
561
+ if (quickWrite) {
562
+ return quickWrite;
563
+ }
431
564
  // Load hub context (AGENTS.md identity, LANDMARKS.md state, project bio if relevant)
432
565
  const hubContext = loadHubContext(message);
433
566
  const hubContextStr = formatHubContext(hubContext);
567
+ this.attachmentContext = this.extractAttachmentContext(message);
434
568
  // Build context from memory
435
569
  const memoryContext = await this.getMemoryContext(message);
436
570
  // Build system prompt with hub context, genesis knowledge, and memory context
@@ -467,6 +601,7 @@ ${hubContextStr}
467
601
  const response = await this.runAgentLoop(messages, systemWithContext, sendMessage, apiKey);
468
602
  // Auto-remember important things from the conversation
469
603
  await this.autoRemember(message, response);
604
+ void this.autoCaptureContext(message);
470
605
  // Log significant actions to hub
471
606
  if (response.length > 100) {
472
607
  const action = message.length > 50 ? message.slice(0, 50) + '...' : message;
@@ -478,16 +613,239 @@ ${hubContextStr}
478
613
  const errorMessage = error instanceof Error ? error.message : String(error);
479
614
  return `Error: ${errorMessage}`;
480
615
  }
616
+ finally {
617
+ this.attachmentContext = null;
618
+ }
619
+ }
620
+ tryHandleCalendarTodo(message) {
621
+ const todoItems = this.extractTodoItems(message);
622
+ const calendarEvents = this.extractCalendarEvents(message);
623
+ if (todoItems.length === 0 && calendarEvents.length === 0)
624
+ return null;
625
+ const results = [];
626
+ if (todoItems.length > 0) {
627
+ const todoRes = this.calendarTools.upsertTodoItems(todoItems);
628
+ if (!todoRes.success) {
629
+ return `Failed to update /todo: ${todoRes.error || todoRes.output || 'unknown error'}`;
630
+ }
631
+ results.push(`Updated /todo with ${todoItems.length} item(s).`);
632
+ }
633
+ if (calendarEvents.length > 0) {
634
+ const calRes = this.calendarTools.upsertCalendarEvents(calendarEvents);
635
+ if (!calRes.success) {
636
+ return `Failed to update /cal: ${calRes.error || calRes.output || 'unknown error'}`;
637
+ }
638
+ results.push(`Updated /cal with ${calendarEvents.length} event(s).`);
639
+ }
640
+ return `${results.join(' ')} Open /todo or /cal to view.`;
641
+ }
642
+ extractTodoItems(message) {
643
+ const lower = message.toLowerCase();
644
+ const hasTodoIntent = (/(\s|^)\/todo\b/i.test(message) || /\bto\s+my\s+todo\b/i.test(message) || /\bto\s+my\s+\/todo\b/i.test(message)) &&
645
+ /\b(add|put|save|store|track)\b/i.test(message);
646
+ if (!hasTodoIntent)
647
+ return [];
648
+ const items = [];
649
+ const wantSteps = /\b(step\s*[- ]?by\s*[- ]?step|break\s*down|smaller\s*steps|parse\s*it)\b/i.test(message);
650
+ const hasRag = /\b(rag|retrieval augmented|retrieval-augmented|embedding|embeddings|pierce)\b/i.test(message);
651
+ const hasWhenIsGood = /\b(whenisgood|when\s+is\s+good|availability|unavailable|schedule|scheduling|lab\s+meeting)\b/i.test(message);
652
+ if (wantSteps && hasRag) {
653
+ items.push({ title: '[RAG] Finish embedding Pierce 2025', priority: 'high' }, { title: '[RAG] Ingest/open-source genetics book content', priority: 'high' }, { title: '[RAG] Chunk + embed genetics book (verify coverage)', priority: 'high' }, { title: '[RAG] Build/refresh retrieval index + metadata', priority: 'medium' }, { title: '[RAG] Smoke test RAG queries + failure cases', priority: 'medium' }, { title: '[RAG] Create student access + usage instructions', priority: 'medium' }, { title: '[RAG] Grant access to students + announce', priority: 'medium' });
654
+ }
655
+ if (wantSteps && hasWhenIsGood) {
656
+ items.push({ title: '[Meetings] Review WhenIsGood results (unavailability)', priority: 'high' }, { title: '[Meetings] Build availability matrix per student', priority: 'high' }, { title: '[Meetings] Identify 2–3 candidate lab meeting slots', priority: 'medium' }, { title: '[Meetings] Decide lab meeting time + confirm with group', priority: 'medium' }, { title: '[Meetings] Pick individual meeting times for each student', priority: 'medium' }, { title: '[Meetings] Send invites/confirmations', priority: 'medium' }, { title: '[Meetings] Add meetings to /cal', priority: 'medium' });
657
+ }
658
+ if (!wantSteps || (items.length === 0 && (hasRag || hasWhenIsGood))) {
659
+ const raw = message.trim().slice(0, 240);
660
+ items.push({
661
+ title: `[Todo] ${raw}${message.trim().length > 240 ? '…' : ''}`,
662
+ priority: 'medium',
663
+ notes: message.trim()
664
+ });
665
+ }
666
+ return items;
667
+ }
668
+ extractCalendarEvents(message) {
669
+ const hasCalendarIntent = /\bto\s+my\s+cal\b/i.test(message) ||
670
+ /\bto\s+my\s+calendar\b/i.test(message) ||
671
+ /(\s|^)\/cal\b/i.test(message) ||
672
+ /(\s|^)\/calendar\b/i.test(message);
673
+ const hasAddIntent = /\b(add|put|schedule|book)\b/i.test(message);
674
+ if (!hasAddIntent)
675
+ return [];
676
+ const candidates = message
677
+ .split(/\n\s*\n/)
678
+ .map((chunk) => chunk.trim())
679
+ .filter(Boolean);
680
+ const events = [];
681
+ for (const chunk of candidates) {
682
+ const parsed = this.parseCalendarEvent(chunk, hasCalendarIntent);
683
+ if (parsed)
684
+ events.push(parsed);
685
+ }
686
+ return events;
687
+ }
688
+ parseCalendarEvent(message, hasCalendarIntent) {
689
+ const explicit = this.parseExplicitDateTimeEvent(message, hasCalendarIntent);
690
+ if (explicit)
691
+ return explicit;
692
+ const weekdayMatch = message.match(/\b(mon(?:day)?|tue(?:sday)?|wed(?:nesday)?|thu(?:rsday)?|fri(?:day)?|sat(?:urday)?|sun(?:day)?)\b/i);
693
+ const timeMatch = message.match(/\b(\d{1,2})(?::(\d{2}))?\s*(a|am|p|pm)\s*-\s*(\d{1,2})(?::(\d{2}))?\s*(a|am|p|pm)\b/i);
694
+ if (!weekdayMatch || !timeMatch)
695
+ return null;
696
+ const weekday = weekdayMatch[1].slice(0, 3).toLowerCase();
697
+ const weekdayIndex = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 }[weekday];
698
+ const startTime = this.to24h(timeMatch[1], timeMatch[2], timeMatch[3]);
699
+ const endTime = this.to24h(timeMatch[4], timeMatch[5], timeMatch[6]);
700
+ const startDate = this.nextOccurrence(weekdayIndex, startTime);
701
+ const dateKey = this.toDateKey(startDate);
702
+ const title = message
703
+ .replace(/^\s*add\s+/i, '')
704
+ .replace(/\s+to\s+\b(mon(?:day)?|tue(?:sday)?|wed(?:nesday)?|thu(?:rsday)?|fri(?:day)?|sat(?:urday)?|sun(?:day)?)\b[\s\S]*$/i, '')
705
+ .trim();
706
+ if (!title)
707
+ return null;
708
+ return {
709
+ title,
710
+ date: dateKey,
711
+ startTime,
712
+ endTime,
713
+ calendarId: 'primary'
714
+ };
715
+ }
716
+ parseExplicitDateTimeEvent(message, hasCalendarIntent) {
717
+ if (!hasCalendarIntent)
718
+ return null;
719
+ // e.g. "Tuesday, January 13, 3:30 pm, A211 PDB—TITLE, \"Talk...\" ... Host: ..."
720
+ const monthMatch = message.match(/\b(jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\b/i);
721
+ if (!monthMatch)
722
+ return null;
723
+ const monthText = monthMatch[1].slice(0, 3).toLowerCase();
724
+ const monthIndex = { jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5, jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11 }[monthText];
725
+ const dayMatch = message.match(new RegExp(`${monthMatch[1]}\\s+(\\d{1,2})`, 'i'));
726
+ const timeMatch = message.match(/\b(\d{1,2})(?::(\d{2}))?\s*(a|am|p|pm)\b/i);
727
+ if (!dayMatch || !timeMatch)
728
+ return null;
729
+ const dayOfMonth = Number(dayMatch[1]);
730
+ const startTime = this.to24h(timeMatch[1], timeMatch[2], timeMatch[3]);
731
+ const yearMatch = message.match(/\b(20\d{2})\b/);
732
+ const now = new Date();
733
+ const year = yearMatch ? Number(yearMatch[1]) : now.getFullYear();
734
+ const date = new Date(year, monthIndex, dayOfMonth, 0, 0, 0, 0);
735
+ const dateKey = this.toDateKey(date);
736
+ const cleaned = message
737
+ .replace(/^\s*add\s+(these\s+)?to\s+my\s+(cal|calendar)\b[:\s-]*/i, '')
738
+ .trim();
739
+ const parts = cleaned.split(/—|--|–/);
740
+ const head = parts[0] || cleaned;
741
+ const after = parts.slice(1).join('—').trim();
742
+ // Try to extract a succinct title from the segment after the location delimiter, otherwise use head.
743
+ const titleCandidate = after ? after : head;
744
+ const title = titleCandidate
745
+ .replace(/^[^A-Za-z0-9]*\b(mon(?:day)?|tue(?:sday)?|wed(?:nesday)?|thu(?:rsday)?|fri(?:day)?|sat(?:urday)?|sun(?:day)?)\b[, ]*/i, '')
746
+ .replace(/\b(jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\b\s+\d{1,2}(?:,\s*\d{4})?/i, '')
747
+ .replace(/\b\d{1,2}(?::\d{2})?\s*(a|am|p|pm)\b/i, '')
748
+ .replace(/^[,\s]+|[,\s]+$/g, '')
749
+ .slice(0, 160);
750
+ if (!title)
751
+ return null;
752
+ const notes = cleaned.length > title.length ? cleaned : undefined;
753
+ return {
754
+ title,
755
+ date: dateKey,
756
+ startTime,
757
+ calendarId: 'primary',
758
+ notes
759
+ };
760
+ }
761
+ to24h(hourText, minuteText, meridiemText) {
762
+ const hour = Number(hourText);
763
+ const minute = Number(minuteText || '0');
764
+ const meridiem = meridiemText.toLowerCase();
765
+ let h = hour % 12;
766
+ if (meridiem.startsWith('p'))
767
+ h += 12;
768
+ return `${String(h).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
769
+ }
770
+ nextOccurrence(weekdayIndex, startTime) {
771
+ const now = new Date();
772
+ const candidate = new Date(now);
773
+ candidate.setHours(0, 0, 0, 0);
774
+ const delta = (weekdayIndex - candidate.getDay() + 7) % 7;
775
+ candidate.setDate(candidate.getDate() + delta);
776
+ if (startTime) {
777
+ const [h, m] = startTime.split(':').map(Number);
778
+ candidate.setHours(h, m, 0, 0);
779
+ if (candidate.getTime() <= now.getTime()) {
780
+ candidate.setDate(candidate.getDate() + 7);
781
+ }
782
+ }
783
+ return candidate;
784
+ }
785
+ toDateKey(date) {
786
+ const year = date.getFullYear();
787
+ const month = `${date.getMonth() + 1}`.padStart(2, '0');
788
+ const day = `${date.getDate()}`.padStart(2, '0');
789
+ return `${year}-${month}-${day}`;
481
790
  }
482
791
  /**
483
792
  * Get relevant memory context for a message
793
+ * Routes based on instanceMode:
794
+ * - personal: search personal memories + all team memories
795
+ * - team: search ONLY team memory (never personal - team Lia is isolated)
484
796
  */
485
797
  async getMemoryContext(message) {
486
798
  try {
799
+ const sections = [];
800
+ // Team mode: ONLY search team memory - never personal
801
+ if (this.instanceMode === 'team') {
802
+ if (!this.orgMemory) {
803
+ return null;
804
+ }
805
+ const teamMemories = await this.orgMemory.search(message, { limit: 10 });
806
+ const teamMemoryLines = teamMemories.map(m => `[team:${m.type}] ${m.content}`).join('\n');
807
+ const teamContextSummary = this.orgContextGraph
808
+ ? await this.orgContextGraph.getContextSummary(message, { limit: 5 })
809
+ : null;
810
+ if (teamMemoryLines)
811
+ sections.push(`TEAM MEMORIES:\n${teamMemoryLines}`);
812
+ if (teamContextSummary)
813
+ sections.push(`TEAM CONTEXT:\n${teamContextSummary}`);
814
+ if (sections.length === 0)
815
+ return null;
816
+ return sections.join('\n\n');
817
+ }
818
+ // Personal mode: search personal + all team memories
487
819
  const memories = await this.memory.search(message, { limit: 5 });
488
- if (memories.length === 0)
820
+ const memoryLines = memories.map(m => `[${m.type}] ${m.content}`).join('\n');
821
+ const contextSummary = await this.contextGraph.getContextSummary(message, { limit: 3 });
822
+ if (memoryLines)
823
+ sections.push(`YOUR MEMORIES:\n${memoryLines}`);
824
+ if (contextSummary)
825
+ sections.push(`YOUR CONTEXT:\n${contextSummary}`);
826
+ // In personal mode, also include all team memories for reading
827
+ for (const [orgId, orgMem] of this.userOrgMemories) {
828
+ try {
829
+ const teamMems = await orgMem.search(message, { limit: 3 });
830
+ if (teamMems.length > 0) {
831
+ const teamLines = teamMems.map(m => `[${m.type}] ${m.content}`).join('\n');
832
+ sections.push(`TEAM (${orgId}) MEMORIES:\n${teamLines}`);
833
+ }
834
+ const teamGraph = this.userOrgContextGraphs.get(orgId);
835
+ if (teamGraph) {
836
+ const teamContext = await teamGraph.getContextSummary(message, { limit: 2 });
837
+ if (teamContext) {
838
+ sections.push(`TEAM (${orgId}) CONTEXT:\n${teamContext}`);
839
+ }
840
+ }
841
+ }
842
+ catch {
843
+ // Skip failed team memory access
844
+ }
845
+ }
846
+ if (sections.length === 0)
489
847
  return null;
490
- return memories.map(m => `[${m.type}] ${m.content}`).join('\n');
848
+ return sections.join('\n\n');
491
849
  }
492
850
  catch {
493
851
  return null;
@@ -495,6 +853,9 @@ ${hubContextStr}
495
853
  }
496
854
  /**
497
855
  * Auto-remember important information from conversations
856
+ * Routes based on instanceMode:
857
+ * - personal: write to personal memory
858
+ * - team: write to team memory
498
859
  */
499
860
  async autoRemember(userMessage, response) {
500
861
  // Look for preference indicators
@@ -502,16 +863,140 @@ ${hubContextStr}
502
863
  const lower = userMessage.toLowerCase();
503
864
  for (const indicator of prefIndicators) {
504
865
  if (lower.includes(indicator)) {
505
- await this.memory.remember(userMessage, {
506
- type: 'semantic',
507
- source: 'auto-extract',
508
- importance: 0.7,
509
- tags: ['preference', 'user']
510
- });
866
+ // Route to correct memory store based on instanceMode
867
+ const targetMemory = this.instanceMode === 'team' ? this.orgMemory : this.memory;
868
+ if (targetMemory) {
869
+ await targetMemory.remember(userMessage, {
870
+ type: 'semantic',
871
+ source: 'auto-extract',
872
+ importance: 0.7,
873
+ tags: ['preference', this.instanceMode === 'team' ? 'team' : 'user']
874
+ });
875
+ }
511
876
  break;
512
877
  }
513
878
  }
514
879
  }
880
+ async autoCaptureContext(userMessage) {
881
+ const marker = '📎 Attached files:';
882
+ const base = userMessage.split(marker)[0]?.trim() || '';
883
+ if (!base || base.startsWith('/'))
884
+ return;
885
+ const text = base.replace(/\s+/g, ' ').trim();
886
+ const shareToOrg = !!this.orgContextGraph &&
887
+ /\b(share with team|team files|shared with team|team-wide|org-wide|company-wide)\b/i.test(text);
888
+ const graphs = shareToOrg && this.orgContextGraph
889
+ ? [this.contextGraph, this.orgContextGraph]
890
+ : [this.contextGraph];
891
+ const seenEntities = new Set();
892
+ const tasks = [];
893
+ const rememberEntity = (kind, name, attributes) => {
894
+ const cleaned = this.normalizeEntityName(name);
895
+ if (!cleaned || this.isIgnoredEntity(cleaned))
896
+ return;
897
+ for (const graph of graphs) {
898
+ const scopeLabel = graph === this.orgContextGraph ? 'org' : 'user';
899
+ const key = `${scopeLabel}:${kind}:${cleaned.toLowerCase()}`;
900
+ if (seenEntities.has(key))
901
+ continue;
902
+ seenEntities.add(key);
903
+ tasks.push(graph.upsertEntity({
904
+ kind,
905
+ name: cleaned,
906
+ attributes,
907
+ tags: ['ctx:auto']
908
+ }));
909
+ }
910
+ };
911
+ const rememberRelation = (from, to, type, fromKind, toKind) => {
912
+ const fromClean = this.normalizeEntityName(from);
913
+ const toClean = this.normalizeEntityName(to);
914
+ if (!fromClean || !toClean || this.isIgnoredEntity(fromClean) || this.isIgnoredEntity(toClean))
915
+ return;
916
+ for (const graph of graphs) {
917
+ tasks.push(graph.linkEntities({
918
+ from: fromClean,
919
+ to: toClean,
920
+ type,
921
+ fromKind,
922
+ toKind,
923
+ source: 'auto-capture'
924
+ }));
925
+ }
926
+ };
927
+ const emailMatches = text.match(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi) || [];
928
+ for (const email of emailMatches.slice(0, 5)) {
929
+ rememberEntity('person', email, { email });
930
+ }
931
+ for (const match of text.matchAll(/\bproject\s+([A-Za-z0-9][\w .'-]{1,60})/gi)) {
932
+ rememberEntity('project', match[1]);
933
+ }
934
+ for (const match of text.matchAll(/\bteam\s+([A-Za-z0-9][\w .'-]{1,50})/gi)) {
935
+ rememberEntity('team', match[1]);
936
+ }
937
+ for (const match of text.matchAll(/\b([A-Za-z0-9][\w .'-]{1,50})\s+team\b/gi)) {
938
+ rememberEntity('team', match[1]);
939
+ }
940
+ for (const match of text.matchAll(/\b(company|org|organization)\s+([A-Za-z0-9][\w .'-]{1,60})/gi)) {
941
+ rememberEntity('org', match[2]);
942
+ }
943
+ for (const match of text.matchAll(/\b(tool|platform|stack|using)\s+([A-Za-z0-9][\w .'-]{1,40})/gi)) {
944
+ rememberEntity('tool', match[2]);
945
+ }
946
+ for (const match of text.matchAll(/([A-Za-z][\w .'-]{1,50})\s+(works on|working on)\s+(?:the\s+)?project\s+([A-Za-z0-9][\w .'-]{1,60})/gi)) {
947
+ rememberRelation(match[1], match[3], 'works_on', 'person', 'project');
948
+ }
949
+ for (const match of text.matchAll(/([A-Za-z][\w .'-]{1,50})\s+owns\s+(?:the\s+)?project\s+([A-Za-z0-9][\w .'-]{1,60})/gi)) {
950
+ rememberRelation(match[1], match[2], 'owns', 'person', 'project');
951
+ }
952
+ for (const match of text.matchAll(/([A-Za-z][\w .'-]{1,50})\s+leads\s+(?:the\s+)?([A-Za-z0-9][\w .'-]{1,50})\s+team/gi)) {
953
+ rememberRelation(match[1], match[2], 'leads', 'person', 'team');
954
+ }
955
+ for (const match of text.matchAll(/([A-Za-z][\w .'-]{1,50})\s+is\s+on\s+the\s+([A-Za-z0-9][\w .'-]{1,50})\s+team/gi)) {
956
+ rememberRelation(match[1], match[2], 'member_of', 'person', 'team');
957
+ }
958
+ for (const match of text.matchAll(/project\s+([A-Za-z0-9][\w .'-]{1,60})\s+depends on\s+project\s+([A-Za-z0-9][\w .'-]{1,60})/gi)) {
959
+ rememberRelation(match[1], match[2], 'depends_on', 'project', 'project');
960
+ }
961
+ for (const match of text.matchAll(/([A-Za-z][\w .'-]{1,50})\s+reports to\s+([A-Za-z0-9][\w .'-]{1,50})/gi)) {
962
+ rememberRelation(match[1], match[2], 'reports_to', 'person', 'person');
963
+ }
964
+ for (const match of text.matchAll(/([A-Za-z][\w .'-]{1,50})\s+uses\s+([A-Za-z0-9][\w .'-]{1,40})/gi)) {
965
+ const subject = match[1];
966
+ const tool = match[2];
967
+ const lowered = subject.toLowerCase();
968
+ let subjectKind = 'person';
969
+ let subjectName = subject;
970
+ if (lowered.includes('team')) {
971
+ subjectKind = 'team';
972
+ subjectName = subject.replace(/team/gi, '').trim();
973
+ }
974
+ else if (lowered.includes('project')) {
975
+ subjectKind = 'project';
976
+ subjectName = subject.replace(/project/gi, '').trim();
977
+ }
978
+ rememberEntity('tool', tool);
979
+ rememberRelation(subjectName, tool, 'uses', subjectKind, 'tool');
980
+ }
981
+ if (tasks.length > 0) {
982
+ try {
983
+ await Promise.all(tasks);
984
+ }
985
+ catch {
986
+ // Best-effort; context capture should never block responses.
987
+ }
988
+ }
989
+ }
990
+ normalizeEntityName(name) {
991
+ return name.replace(/["'`]/g, '').replace(/\s+/g, ' ').trim();
992
+ }
993
+ isIgnoredEntity(name) {
994
+ if (!name || name.length < 2 || name.length > 80)
995
+ return true;
996
+ const lower = name.toLowerCase();
997
+ const ignored = new Set(['i', 'we', 'you', 'they', 'he', 'she', 'it', 'our', 'my', 'your', 'their']);
998
+ return ignored.has(lower);
999
+ }
515
1000
  /**
516
1001
  * Run the agentic loop
517
1002
  * @param apiKey Optional per-request API key (creates temporary client)
@@ -521,17 +1006,28 @@ ${hubContextStr}
521
1006
  let currentMessages = [...messages];
522
1007
  let iterations = 0;
523
1008
  const maxIterations = 30; // Increased for complex multi-tool tasks
1009
+ const requestTimeoutMs = 60000;
524
1010
  // Use per-request client if apiKey provided, otherwise use default
525
1011
  const client = apiKey ? new Anthropic({ apiKey }) : this.client;
526
1012
  while (iterations < maxIterations) {
527
1013
  iterations++;
528
- const response = await client.messages.create({
529
- model: this.model,
530
- max_tokens: this.maxTokens,
531
- system,
532
- tools,
533
- messages: currentMessages
534
- });
1014
+ const requestStart = Date.now();
1015
+ console.log(`[ORCHESTRATOR] Anthropic request ${iterations}/${maxIterations} (model: ${this.model})`);
1016
+ const response = await Promise.race([
1017
+ client.messages.create({
1018
+ model: this.model,
1019
+ max_tokens: this.maxTokens,
1020
+ system,
1021
+ tools,
1022
+ messages: currentMessages
1023
+ }),
1024
+ new Promise((_, reject) => {
1025
+ setTimeout(() => {
1026
+ reject(new Error(`Anthropic request timed out after ${requestTimeoutMs}ms`));
1027
+ }, requestTimeoutMs);
1028
+ })
1029
+ ]);
1030
+ console.log(`[ORCHESTRATOR] Anthropic response ${iterations}/${maxIterations} in ${Date.now() - requestStart}ms`);
535
1031
  // Check if done
536
1032
  if (response.stop_reason === 'end_turn' || !this.hasToolUse(response.content)) {
537
1033
  return this.extractText(response.content);
@@ -576,6 +1072,87 @@ For ANY file creation, editing, or system modification, use delegate_to_worker i
576
1072
  required: ['command']
577
1073
  }
578
1074
  },
1075
+ {
1076
+ name: 'calendar_upsert_events',
1077
+ description: `Create or update calendar events for /cal.
1078
+
1079
+ Writes ONLY to ~/Lia-Hub/shared/outputs/.lia/calendar.json (creates the directory/file if missing).`,
1080
+ input_schema: {
1081
+ type: 'object',
1082
+ properties: {
1083
+ events: {
1084
+ type: 'array',
1085
+ items: {
1086
+ type: 'object',
1087
+ properties: {
1088
+ id: { type: 'string' },
1089
+ title: { type: 'string' },
1090
+ date: { type: 'string', description: 'YYYY-MM-DD' },
1091
+ startTime: { type: 'string', description: 'HH:MM (24h) or ISO timestamp' },
1092
+ endTime: { type: 'string', description: 'HH:MM (24h) or ISO timestamp' },
1093
+ calendarId: { type: 'string' },
1094
+ notes: { type: 'string' }
1095
+ },
1096
+ required: ['title']
1097
+ }
1098
+ },
1099
+ calendars: {
1100
+ type: 'array',
1101
+ items: {
1102
+ type: 'object',
1103
+ properties: {
1104
+ id: { type: 'string' },
1105
+ name: { type: 'string' },
1106
+ color: { type: 'string' }
1107
+ },
1108
+ required: ['id', 'name']
1109
+ },
1110
+ description: 'Optional calendar definitions; omitted to keep existing.'
1111
+ }
1112
+ },
1113
+ required: ['events']
1114
+ }
1115
+ },
1116
+ {
1117
+ name: 'todo_upsert_items',
1118
+ description: `Create or update todo items for /todo.
1119
+
1120
+ Writes ONLY to ~/Lia-Hub/shared/outputs/.lia/todos.json (creates the directory/file if missing).`,
1121
+ input_schema: {
1122
+ type: 'object',
1123
+ properties: {
1124
+ items: {
1125
+ type: 'array',
1126
+ items: {
1127
+ type: 'object',
1128
+ properties: {
1129
+ id: { type: 'string' },
1130
+ title: { type: 'string' },
1131
+ done: { type: 'boolean' },
1132
+ due: { type: 'string' },
1133
+ priority: { type: 'string', enum: ['low', 'medium', 'high'] },
1134
+ notes: { type: 'string' },
1135
+ subtasks: {
1136
+ type: 'array',
1137
+ items: {
1138
+ type: 'object',
1139
+ properties: {
1140
+ id: { type: 'string' },
1141
+ title: { type: 'string' },
1142
+ done: { type: 'boolean' },
1143
+ notes: { type: 'string' }
1144
+ },
1145
+ required: ['title']
1146
+ }
1147
+ }
1148
+ },
1149
+ required: ['title']
1150
+ }
1151
+ }
1152
+ },
1153
+ required: ['items']
1154
+ }
1155
+ },
579
1156
  // Web tools
580
1157
  {
581
1158
  name: 'web_search',
@@ -759,6 +1336,110 @@ Be specific about what you want done.`,
759
1336
  required: ['content']
760
1337
  }
761
1338
  },
1339
+ {
1340
+ name: 'remember_entity',
1341
+ description: 'Store or update a team context entity (person, team, project, tool, task, doc).',
1342
+ input_schema: {
1343
+ type: 'object',
1344
+ properties: {
1345
+ kind: {
1346
+ type: 'string',
1347
+ enum: ContextGraphKinds.entities,
1348
+ description: 'Entity kind'
1349
+ },
1350
+ name: {
1351
+ type: 'string',
1352
+ description: 'Entity name'
1353
+ },
1354
+ description: {
1355
+ type: 'string',
1356
+ description: 'Short description or role'
1357
+ },
1358
+ attributes: {
1359
+ type: 'object',
1360
+ description: 'Optional key/value attributes',
1361
+ additionalProperties: { type: 'string' }
1362
+ },
1363
+ tags: {
1364
+ type: 'array',
1365
+ items: { type: 'string' },
1366
+ description: 'Optional tags'
1367
+ },
1368
+ scope: {
1369
+ type: 'string',
1370
+ enum: ['user', 'org'],
1371
+ description: 'Where to store the entity (default: user)'
1372
+ }
1373
+ },
1374
+ required: ['kind', 'name']
1375
+ }
1376
+ },
1377
+ {
1378
+ name: 'remember_relation',
1379
+ description: 'Link two entities with a relationship (depends_on, works_on, member_of, etc).',
1380
+ input_schema: {
1381
+ type: 'object',
1382
+ properties: {
1383
+ from: {
1384
+ type: 'string',
1385
+ description: 'Entity name or key'
1386
+ },
1387
+ to: {
1388
+ type: 'string',
1389
+ description: 'Entity name or key'
1390
+ },
1391
+ type: {
1392
+ type: 'string',
1393
+ enum: ContextGraphKinds.relations,
1394
+ description: 'Relationship type'
1395
+ },
1396
+ from_kind: {
1397
+ type: 'string',
1398
+ enum: ContextGraphKinds.entities,
1399
+ description: 'Optional kind hint for from'
1400
+ },
1401
+ to_kind: {
1402
+ type: 'string',
1403
+ enum: ContextGraphKinds.entities,
1404
+ description: 'Optional kind hint for to'
1405
+ },
1406
+ description: {
1407
+ type: 'string',
1408
+ description: 'Optional relation detail'
1409
+ },
1410
+ scope: {
1411
+ type: 'string',
1412
+ enum: ['user', 'org'],
1413
+ description: 'Where to store the relation (default: user)'
1414
+ }
1415
+ },
1416
+ required: ['from', 'to', 'type']
1417
+ }
1418
+ },
1419
+ {
1420
+ name: 'search_context',
1421
+ description: 'Search the context graph for entities and relationships.',
1422
+ input_schema: {
1423
+ type: 'object',
1424
+ properties: {
1425
+ query: {
1426
+ type: 'string',
1427
+ description: 'What to search for'
1428
+ },
1429
+ kind: {
1430
+ type: 'string',
1431
+ enum: ContextGraphKinds.entities,
1432
+ description: 'Optional entity kind filter'
1433
+ },
1434
+ scope: {
1435
+ type: 'string',
1436
+ enum: ['user', 'org'],
1437
+ description: 'Which context scope to search (default: user)'
1438
+ }
1439
+ },
1440
+ required: ['query']
1441
+ }
1442
+ },
762
1443
  {
763
1444
  name: 'search_memory',
764
1445
  description: 'Search your memory for relevant information. Use before delegating to include user context.',
@@ -882,6 +1563,12 @@ Be specific about what you want done.`,
882
1563
  case 'bash':
883
1564
  result = await this.bashTool.execute(input.command, input.timeout);
884
1565
  break;
1566
+ case 'calendar_upsert_events':
1567
+ result = this.calendarTools.upsertCalendarEvents(input.events, input.calendars);
1568
+ break;
1569
+ case 'todo_upsert_items':
1570
+ result = this.calendarTools.upsertTodoItems(input.items);
1571
+ break;
885
1572
  case 'web_search':
886
1573
  result = await this.webTools.webSearch(input.query, { numResults: input.num_results });
887
1574
  break;
@@ -892,12 +1579,14 @@ Be specific about what you want done.`,
892
1579
  });
893
1580
  break;
894
1581
  // Worker tools
895
- case 'spawn_worker':
896
- result = await this.workerTools.spawnWorker(input.task, {
1582
+ case 'spawn_worker': {
1583
+ const { task } = this.withAttachments(input.task);
1584
+ result = await this.workerTools.spawnWorker(task, {
897
1585
  timeout: input.timeout,
898
1586
  priority: input.priority
899
1587
  });
900
1588
  break;
1589
+ }
901
1590
  case 'check_worker':
902
1591
  result = await this.workerTools.checkWorker(input.job_id);
903
1592
  break;
@@ -911,12 +1600,68 @@ Be specific about what you want done.`,
911
1600
  result = await this.workerTools.cancelWorker(input.job_id);
912
1601
  break;
913
1602
  // Legacy delegate tool
914
- case 'delegate_to_worker':
915
- result = await this.delegateToWorker(input.task, input.context, input.working_directory);
1603
+ case 'delegate_to_worker': {
1604
+ const { task, context } = this.withAttachments(input.task, input.context);
1605
+ result = await this.delegateToWorker(task, context, input.working_directory);
916
1606
  break;
1607
+ }
917
1608
  case 'remember':
918
1609
  result = await this.executeRemember(input.content, input.type, input.importance);
919
1610
  break;
1611
+ case 'remember_entity': {
1612
+ const scope = input.scope === 'org' ? 'org' : 'user';
1613
+ const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
1614
+ if (!graph) {
1615
+ result = { success: false, output: 'Org context is not configured for this agent.' };
1616
+ break;
1617
+ }
1618
+ const entity = await graph.upsertEntity({
1619
+ kind: input.kind,
1620
+ name: input.name,
1621
+ description: input.description,
1622
+ attributes: input.attributes,
1623
+ tags: input.tags
1624
+ });
1625
+ result = { success: true, output: `Stored ${scope} entity: ${entity.kind} ${entity.name} (${entity.key})` };
1626
+ break;
1627
+ }
1628
+ case 'remember_relation': {
1629
+ const scope = input.scope === 'org' ? 'org' : 'user';
1630
+ const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
1631
+ if (!graph) {
1632
+ result = { success: false, output: 'Org context is not configured for this agent.' };
1633
+ break;
1634
+ }
1635
+ const relation = await graph.linkEntities({
1636
+ from: input.from,
1637
+ to: input.to,
1638
+ type: input.type,
1639
+ description: input.description,
1640
+ fromKind: input.from_kind,
1641
+ toKind: input.to_kind
1642
+ });
1643
+ result = { success: true, output: `Linked ${scope}: ${relation.from} ${relation.type} ${relation.to}` };
1644
+ break;
1645
+ }
1646
+ case 'search_context': {
1647
+ const scope = input.scope === 'org' ? 'org' : 'user';
1648
+ const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
1649
+ if (!graph) {
1650
+ result = { success: false, output: 'Org context is not configured for this agent.' };
1651
+ break;
1652
+ }
1653
+ const entities = await graph.searchEntities(input.query, { kind: input.kind, limit: 6 });
1654
+ if (entities.length === 0) {
1655
+ result = { success: true, output: 'No context entities found.' };
1656
+ break;
1657
+ }
1658
+ const lines = entities.map(entity => {
1659
+ const desc = entity.description ? ` - ${entity.description}` : '';
1660
+ return `[${entity.kind}] ${entity.name}${desc} (${entity.key})`;
1661
+ });
1662
+ result = { success: true, output: lines.join('\n') };
1663
+ break;
1664
+ }
920
1665
  case 'search_memory':
921
1666
  result = await this.executeSearchMemory(input.query, input.type);
922
1667
  break;
@@ -999,10 +1744,31 @@ Be specific about what you want done.`,
999
1744
  };
1000
1745
  // Escape single quotes in prompt for shell safety
1001
1746
  const escapedPrompt = prompt.replace(/'/g, "'\\''");
1747
+ const claudeCmd = `${this.claudePath} -p '${escapedPrompt}' --output-format text --dangerously-skip-permissions`;
1748
+ const isRoot = process.getuid?.() === 0;
1749
+ const shellCmd = isRoot
1750
+ ? `runuser -u lia -m -- /bin/bash -l -c "${claudeCmd.replace(/"/g, '\\"')}"`
1751
+ : claudeCmd;
1752
+ let workerUser = process.env.USER || 'lia';
1753
+ let workerLogname = process.env.LOGNAME || workerUser;
1754
+ if (isRoot) {
1755
+ workerUser = 'lia';
1756
+ workerLogname = 'lia';
1757
+ }
1758
+ const spawnEnv = {
1759
+ ...process.env,
1760
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
1761
+ GH_TOKEN: process.env.GH_TOKEN,
1762
+ GITHUB_TOKEN: process.env.GITHUB_TOKEN,
1763
+ HOME: cwd,
1764
+ LIA_WORKSPACE: cwd,
1765
+ USER: workerUser,
1766
+ LOGNAME: workerLogname
1767
+ };
1002
1768
  // Spawn worker
1003
- const child = spawn('/bin/bash', ['-l', '-c', `claude -p '${escapedPrompt}' --dangerously-skip-permissions`], {
1769
+ const child = spawn('/bin/bash', ['-l', '-c', shellCmd], {
1004
1770
  cwd,
1005
- env: { ...process.env },
1771
+ env: spawnEnv,
1006
1772
  stdio: ['pipe', 'pipe', 'pipe']
1007
1773
  });
1008
1774
  job.process = child;
@@ -1024,12 +1790,18 @@ Be specific about what you want done.`,
1024
1790
  job.output += text;
1025
1791
  // Stream output to console
1026
1792
  process.stdout.write(`[${id}] ${text}`);
1793
+ if (this.workerLogCallback) {
1794
+ this.workerLogCallback(id, text, 'stdout');
1795
+ }
1027
1796
  });
1028
1797
  child.stderr?.on('data', (data) => {
1029
1798
  const text = data.toString();
1030
1799
  if (!text.includes('Checking') && !text.includes('Connected')) {
1031
1800
  job.output += text;
1032
1801
  }
1802
+ if (this.workerLogCallback) {
1803
+ this.workerLogCallback(id, text, 'stderr');
1804
+ }
1033
1805
  });
1034
1806
  child.on('close', async (code) => {
1035
1807
  clearTimeout(timeout);
@@ -1196,9 +1968,24 @@ Be specific about what you want done.`,
1196
1968
  }
1197
1969
  /**
1198
1970
  * Execute remember tool
1971
+ * Routes writes based on instanceMode:
1972
+ * - personal: write to personal memory only
1973
+ * - team: write to team memory only
1199
1974
  */
1200
1975
  async executeRemember(content, type, importance) {
1201
1976
  try {
1977
+ // Team mode: write to org memory only
1978
+ if (this.instanceMode === 'team') {
1979
+ if (!this.orgMemory) {
1980
+ return { success: false, output: 'Team memory not configured. Cannot store team memory.' };
1981
+ }
1982
+ const id = await this.orgMemory.remember(content, {
1983
+ type: type || undefined,
1984
+ importance
1985
+ });
1986
+ return { success: true, output: `Remembered in team memory (${id})` };
1987
+ }
1988
+ // Personal mode: write to personal memory only
1202
1989
  const id = await this.memory.remember(content, {
1203
1990
  type: type || undefined,
1204
1991
  importance
@@ -1211,17 +1998,53 @@ Be specific about what you want done.`,
1211
1998
  }
1212
1999
  /**
1213
2000
  * Execute search memory tool
2001
+ * Routes searches based on instanceMode:
2002
+ * - personal: search personal + all team memories
2003
+ * - team: search team memory only (never personal)
1214
2004
  */
1215
2005
  async executeSearchMemory(query, type) {
1216
2006
  try {
1217
- const memories = await this.memory.search(query, {
2007
+ const searchOpts = {
1218
2008
  type: type || undefined,
1219
2009
  limit: 10
1220
- });
1221
- if (memories.length === 0) {
2010
+ };
2011
+ // Team mode: search only team memory
2012
+ if (this.instanceMode === 'team') {
2013
+ if (!this.orgMemory) {
2014
+ return { success: false, output: 'Team memory not configured.' };
2015
+ }
2016
+ const memories = await this.orgMemory.search(query, searchOpts);
2017
+ if (memories.length === 0) {
2018
+ return { success: true, output: 'No relevant team memories found.' };
2019
+ }
2020
+ const output = memories.map(m => `[team:${m.type}] (${(m.importance * 100).toFixed(0)}%) ${m.content}`).join('\n\n');
2021
+ return { success: true, output };
2022
+ }
2023
+ // Personal mode: search personal + all team memories
2024
+ const allMemories = [];
2025
+ // Search personal memories
2026
+ const personalMemories = await this.memory.search(query, { ...searchOpts, limit: 5 });
2027
+ for (const m of personalMemories) {
2028
+ allMemories.push({ source: 'personal', memory: m });
2029
+ }
2030
+ // Search all team memories user belongs to
2031
+ for (const [orgId, orgMem] of this.userOrgMemories) {
2032
+ try {
2033
+ const teamMems = await orgMem.search(query, { ...searchOpts, limit: 3 });
2034
+ for (const m of teamMems) {
2035
+ allMemories.push({ source: `team:${orgId}`, memory: m });
2036
+ }
2037
+ }
2038
+ catch {
2039
+ // Skip failed team memory searches
2040
+ }
2041
+ }
2042
+ if (allMemories.length === 0) {
1222
2043
  return { success: true, output: 'No relevant memories found.' };
1223
2044
  }
1224
- const output = memories.map(m => `[${m.type}] (${(m.importance * 100).toFixed(0)}%) ${m.content}`).join('\n\n');
2045
+ // Sort by importance and format
2046
+ allMemories.sort((a, b) => b.memory.importance - a.memory.importance);
2047
+ const output = allMemories.map(({ source, memory: m }) => `[${source}:${m.type}] (${(m.importance * 100).toFixed(0)}%) ${m.content}`).join('\n\n');
1225
2048
  return { success: true, output };
1226
2049
  }
1227
2050
  catch (error) {
@@ -1396,6 +2219,12 @@ Be specific about what you want done.`,
1396
2219
  return `Delegating: ${input.task.slice(0, 60)}...`;
1397
2220
  case 'remember':
1398
2221
  return `Remembering: ${input.content.slice(0, 50)}...`;
2222
+ case 'remember_entity':
2223
+ return `Remembering entity: ${input.name}`;
2224
+ case 'remember_relation':
2225
+ return `Linking context: ${input.from} -> ${input.type} -> ${input.to}`;
2226
+ case 'search_context':
2227
+ return `Searching context: ${input.query}`;
1399
2228
  case 'search_memory':
1400
2229
  return `Searching memory: ${input.query}`;
1401
2230
  case 'schedule_task':
@@ -1438,5 +2267,24 @@ Be specific about what you want done.`,
1438
2267
  else {
1439
2268
  this.memory.close();
1440
2269
  }
2270
+ if (this.orgMemory) {
2271
+ if (this.orgMemory instanceof PostgresMemoryStore) {
2272
+ await this.orgMemory.close();
2273
+ }
2274
+ else {
2275
+ this.orgMemory.close();
2276
+ }
2277
+ }
2278
+ // Close all user org memory stores (for personal mode reading team memories)
2279
+ for (const [, orgMem] of this.userOrgMemories) {
2280
+ if (orgMem instanceof PostgresMemoryStore) {
2281
+ await orgMem.close();
2282
+ }
2283
+ else {
2284
+ orgMem.close();
2285
+ }
2286
+ }
2287
+ this.userOrgMemories.clear();
2288
+ this.userOrgContextGraphs.clear();
1441
2289
  }
1442
2290
  }