@siftd/connect-agent 0.2.37 → 0.2.39

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,13 +11,16 @@ 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';
20
22
  import { buildWorkerPrompt } from './prompts/worker-system.js';
23
+ import { LiaTaskQueue } from './core/task-queue.js';
21
24
  /**
22
25
  * Extract file paths from worker output
23
26
  * Workers naturally mention files they create: "Created /tmp/foo.html", "Saved to /path/file"
@@ -72,6 +75,14 @@ YOUR IDENTITY:
72
75
  HOW YOU WORK:
73
76
  You are the orchestrator, not the executor. You coordinate and remember while workers do the hands-on work.
74
77
 
78
+ TASK QUEUE (Always-Listening):
79
+ You have an internal task queue. Users can keep sending messages while you work - you're interruptible.
80
+ - Use lia_plan to break down complex goals into steps
81
+ - Use lia_add_task to queue follow-up work or new requests that come in
82
+ - Use lia_get_queue to see what's pending
83
+ - When you receive multiple requests, acknowledge them and queue them: "Got it, adding to my queue..."
84
+ - You can work on one task while others wait - always stay responsive
85
+
75
86
  TOOL RULES:
76
87
 
77
88
  ✅ bash - READ-ONLY operations only:
@@ -90,11 +101,21 @@ TOOL RULES:
90
101
  - Building, testing, deploying
91
102
  - Any filesystem modification
92
103
 
104
+ ATTACHMENTS:
105
+ - If the user message includes "📎 Attached files:", include those URLs verbatim in any worker task.
106
+ - Workers should download the files from the URLs and proceed without asking for the content again.
107
+
93
108
  MEMORY:
94
109
  - Search memory before starting work (context is valuable)
95
110
  - Remember important things after significant actions
96
111
  - Learn from failures - what went wrong, how to avoid it
97
112
 
113
+ CONTEXT GRAPH:
114
+ - Use remember_entity to store people, teams, projects, tools, tasks
115
+ - Use remember_relation to link entities (owns, works_on, depends_on, etc.)
116
+ - Use search_context to retrieve team context before asking users to repeat
117
+ - If user asks for "/context", call search_context and summarize results
118
+
98
119
  YOUR HUB: ~/Lia-Hub/
99
120
  - **CLAUDE.md** - User's instructions (always follow these)
100
121
  - **LANDMARKS.md** - Current state and context
@@ -109,22 +130,52 @@ The Lia interface has a built-in gallery that shows worker outputs automatically
109
130
  ✅ Workers write to ~/Lia-Hub/shared/outputs/
110
131
  ✅ Gallery displays assets in-app automatically
111
132
 
133
+ FILES BROWSER:
134
+ Users can type /files to open the Finder UI (cloud mode).
135
+ When asked to browse or locate files, point them to /files.
136
+ Refer to /files instead of internal Lia-Hub paths in responses.
137
+
138
+ FILES SCOPES:
139
+ - "My Files" are private to the user (default /files view)
140
+ - "Team Files" are shared across the org (use /files team or the Team Files location)
141
+ - If a user asks to share with the team, store outputs in Team Files and mention that it is shared
142
+
143
+ CALENDAR + TODO (Lia-managed data):
144
+ - /cal reads ~/Lia-Hub/shared/outputs/.lia/calendar.json
145
+ - /todo reads ~/Lia-Hub/shared/outputs/.lia/todos.json
146
+ - When users add, edit, or delete events, use calendar_upsert_events
147
+ - When users add, edit, or delete tasks, use todo_upsert_items
148
+ - Keep these files hidden; refer users to /cal or /todo in responses
149
+ - Schemas:
150
+ - calendar.json: { "version": 1, "calendars": [...], "events": [...] }
151
+ - todos.json: { "version": 1, "items": [...] }
152
+
112
153
  WORKFLOW:
113
154
  Before complex work: Check CLAUDE.md → Read LANDMARKS.md → Search memory
114
155
  After completing work: Update LANDMARKS.md → Remember learnings
115
156
 
116
- You orchestrate through workers. You remember through memory. You never do file operations directly.`;
157
+ 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
158
  export class MasterOrchestrator {
118
159
  client;
119
160
  model;
120
161
  maxTokens;
121
162
  memory;
163
+ contextGraph;
164
+ orgMemory;
165
+ orgContextGraph;
166
+ orgId;
167
+ orgRole;
168
+ instanceMode;
169
+ userOrgMemories;
170
+ userOrgContextGraphs;
122
171
  scheduler;
123
172
  indexer;
124
173
  jobs = new Map();
125
174
  jobCounter = 0;
126
175
  workerStatusCallback = null;
176
+ workerLogCallback = null;
127
177
  workerStatusInterval = null;
178
+ attachmentContext = null;
128
179
  galleryCallback = null;
129
180
  userId;
130
181
  workspaceDir;
@@ -134,17 +185,28 @@ export class MasterOrchestrator {
134
185
  bashTool;
135
186
  webTools;
136
187
  workerTools;
188
+ calendarTools;
137
189
  sharedState;
138
190
  verboseMode = new Map(); // per-user verbose mode
191
+ // Lia's internal task queue - allows async message processing
192
+ taskQueue;
193
+ taskUpdateCallback;
194
+ planUpdateCallback;
139
195
  constructor(options) {
140
196
  this.client = new Anthropic({ apiKey: options.apiKey });
141
197
  this.model = options.model || 'claude-sonnet-4-20250514';
142
198
  this.maxTokens = options.maxTokens || 4096;
143
199
  this.userId = options.userId;
200
+ this.orgId = options.orgId;
201
+ this.orgRole = options.orgRole;
202
+ this.instanceMode = options.instanceMode || 'personal';
144
203
  this.workspaceDir = options.workspaceDir || process.env.HOME || '/tmp';
204
+ this.userOrgMemories = new Map();
205
+ this.userOrgContextGraphs = new Map();
145
206
  // Find claude binary - critical for worker delegation
146
207
  this.claudePath = this.findClaudeBinary();
147
208
  console.log(`[ORCHESTRATOR] Claude binary: ${this.claudePath}`);
209
+ console.log(`[ORCHESTRATOR] Instance mode: ${this.instanceMode}`);
148
210
  // Initialize memory - use Postgres if DATABASE_URL is set, otherwise SQLite
149
211
  if (isPostgresConfigured()) {
150
212
  console.log('[ORCHESTRATOR] DATABASE_URL detected, using PostgreSQL + pgvector');
@@ -160,6 +222,44 @@ export class MasterOrchestrator {
160
222
  voyageApiKey: options.voyageApiKey
161
223
  });
162
224
  }
225
+ this.contextGraph = new ContextGraph(this.memory, { scope: options.userId });
226
+ // Initialize org memory for team mode or when orgId is provided
227
+ if (this.orgId) {
228
+ if (isPostgresConfigured()) {
229
+ this.orgMemory = new PostgresMemoryStore({
230
+ connectionString: process.env.DATABASE_URL,
231
+ userId: `org:${this.orgId}`
232
+ });
233
+ }
234
+ else {
235
+ this.orgMemory = new AdvancedMemoryStore({
236
+ dbPath: `memories_org_${this.orgId}.db`,
237
+ vectorPath: `./memory_vectors_org_${this.orgId}`,
238
+ voyageApiKey: options.voyageApiKey
239
+ });
240
+ }
241
+ this.orgContextGraph = new ContextGraph(this.orgMemory, { scope: `org:${this.orgId}` });
242
+ }
243
+ // For personal mode: initialize memory stores for all user's teams (for reading)
244
+ if (this.instanceMode === 'personal' && options.userOrgIds) {
245
+ for (const orgId of options.userOrgIds) {
246
+ if (isPostgresConfigured()) {
247
+ this.userOrgMemories.set(orgId, new PostgresMemoryStore({
248
+ connectionString: process.env.DATABASE_URL,
249
+ userId: `org:${orgId}`
250
+ }));
251
+ }
252
+ else {
253
+ this.userOrgMemories.set(orgId, new AdvancedMemoryStore({
254
+ dbPath: `memories_org_${orgId}.db`,
255
+ vectorPath: `./memory_vectors_org_${orgId}`,
256
+ voyageApiKey: options.voyageApiKey
257
+ }));
258
+ }
259
+ this.userOrgContextGraphs.set(orgId, new ContextGraph(this.userOrgMemories.get(orgId), { scope: `org:${orgId}` }));
260
+ }
261
+ console.log(`[ORCHESTRATOR] Personal mode: initialized ${this.userOrgMemories.size} team memory stores for reading`);
262
+ }
163
263
  this.scheduler = new TaskScheduler(`scheduled_${options.userId}.json`);
164
264
  // Create indexer (will index on first initialize call)
165
265
  this.indexer = new SystemIndexer(this.memory);
@@ -167,7 +267,19 @@ export class MasterOrchestrator {
167
267
  this.bashTool = new BashTool(this.workspaceDir);
168
268
  this.webTools = new WebTools();
169
269
  this.workerTools = new WorkerTools(this.workspaceDir);
270
+ this.calendarTools = new CalendarTools();
170
271
  this.sharedState = new SharedState(this.workspaceDir);
272
+ this.workerTools.setLogCallback((workerId, line, stream) => {
273
+ if (this.workerLogCallback) {
274
+ this.workerLogCallback(workerId, line, stream);
275
+ }
276
+ });
277
+ // Initialize Lia's internal task queue
278
+ this.taskQueue = new LiaTaskQueue({
279
+ onTaskUpdate: (task) => this.taskUpdateCallback?.(task),
280
+ onPlanUpdate: (plan) => this.planUpdateCallback?.(plan),
281
+ processTask: async (task) => this.processQueuedTask(task),
282
+ });
171
283
  }
172
284
  /**
173
285
  * Initialize the orchestrator - indexes filesystem on first call
@@ -208,6 +320,12 @@ export class MasterOrchestrator {
208
320
  }
209
321
  }
210
322
  }
323
+ /**
324
+ * Set callback for worker log streaming
325
+ */
326
+ setWorkerLogCallback(callback) {
327
+ this.workerLogCallback = callback;
328
+ }
211
329
  /**
212
330
  * Set callback for gallery updates (worker assets for UI gallery view)
213
331
  */
@@ -219,6 +337,75 @@ export class MasterOrchestrator {
219
337
  this.broadcastGalleryUpdate();
220
338
  } : null);
221
339
  }
340
+ /**
341
+ * Set callback for task queue updates (for real-time UI updates)
342
+ */
343
+ setTaskUpdateCallback(callback) {
344
+ this.taskUpdateCallback = callback || undefined;
345
+ }
346
+ /**
347
+ * Set callback for plan updates (for real-time UI updates)
348
+ */
349
+ setPlanUpdateCallback(callback) {
350
+ this.planUpdateCallback = callback || undefined;
351
+ }
352
+ /**
353
+ * Queue a message for async processing (non-blocking)
354
+ * Returns immediately with task ID - use callbacks or getTaskQueueStatus for results
355
+ */
356
+ queueMessage(message, options) {
357
+ return this.taskQueue.addTask({
358
+ type: 'user_message',
359
+ content: message,
360
+ priority: options?.priority || 'normal',
361
+ metadata: {
362
+ userId: this.userId,
363
+ messageId: options?.messageId,
364
+ apiKey: options?.apiKey,
365
+ context: this.instanceMode,
366
+ orgId: this.orgId,
367
+ },
368
+ });
369
+ }
370
+ /**
371
+ * Process a task from the queue (called by LiaTaskQueue)
372
+ */
373
+ async processQueuedTask(task) {
374
+ // This is where the actual work happens
375
+ return this.processMessage(task.content, [], // Fresh conversation for each task
376
+ undefined, // No sendMessage callback for queued tasks
377
+ task.metadata?.apiKey);
378
+ }
379
+ /**
380
+ * Get task queue status (for UI)
381
+ */
382
+ getTaskQueueStatus() {
383
+ return this.taskQueue.getStatus();
384
+ }
385
+ /**
386
+ * Get pending tasks
387
+ */
388
+ getPendingTasks() {
389
+ return this.taskQueue.getPendingTasks();
390
+ }
391
+ /**
392
+ * Get task by ID
393
+ */
394
+ getTask(taskId) {
395
+ return this.taskQueue.getTask(taskId);
396
+ }
397
+ /**
398
+ * Cancel a pending task
399
+ */
400
+ cancelTask(taskId) {
401
+ return this.taskQueue.cancelTask(taskId);
402
+ }
403
+ /**
404
+ * Get Lia's internal todo list formatted for context
405
+ */
406
+ getTaskQueueContext() {
407
+ return this.taskQueue.formatAsTodoList();
408
+ }
222
409
  /**
223
410
  * Get gallery workers with their assets
224
411
  */
@@ -412,6 +599,35 @@ export class MasterOrchestrator {
412
599
  }
413
600
  return null;
414
601
  }
602
+ extractAttachmentContext(message) {
603
+ const marker = '📎 Attached files:';
604
+ const index = message.indexOf(marker);
605
+ if (index === -1)
606
+ return null;
607
+ const raw = message.slice(index + marker.length);
608
+ const lines = raw
609
+ .split('\n')
610
+ .map(line => line.trim())
611
+ .filter(Boolean);
612
+ if (lines.length === 0)
613
+ return null;
614
+ return lines.join('\n');
615
+ }
616
+ withAttachments(task, context) {
617
+ if (!this.attachmentContext)
618
+ return { task, context };
619
+ const attachmentBlock = this.attachmentContext;
620
+ const hasAttachments = task.includes(attachmentBlock) ||
621
+ task.includes('Attached files') ||
622
+ (context && (context.includes(attachmentBlock) || context.includes('Attached files')));
623
+ if (hasAttachments)
624
+ return { task, context };
625
+ const attachmentNote = `Attached files (download these URLs and use their contents; do not ask the user to resend):\n${attachmentBlock}`;
626
+ if (context && context.trim().length > 0) {
627
+ return { task, context: `${context}\n\n${attachmentNote}` };
628
+ }
629
+ return { task: `${task}\n\n${attachmentNote}` };
630
+ }
415
631
  /**
416
632
  * Check if verbose mode is enabled
417
633
  */
@@ -428,9 +644,15 @@ export class MasterOrchestrator {
428
644
  if (slashResponse) {
429
645
  return slashResponse;
430
646
  }
647
+ // Deterministic calendar/todo writes (do not rely on the model calling tools)
648
+ const quickWrite = this.tryHandleCalendarTodo(message);
649
+ if (quickWrite) {
650
+ return quickWrite;
651
+ }
431
652
  // Load hub context (AGENTS.md identity, LANDMARKS.md state, project bio if relevant)
432
653
  const hubContext = loadHubContext(message);
433
654
  const hubContextStr = formatHubContext(hubContext);
655
+ this.attachmentContext = this.extractAttachmentContext(message);
434
656
  // Build context from memory
435
657
  const memoryContext = await this.getMemoryContext(message);
436
658
  // Build system prompt with hub context, genesis knowledge, and memory context
@@ -467,6 +689,7 @@ ${hubContextStr}
467
689
  const response = await this.runAgentLoop(messages, systemWithContext, sendMessage, apiKey);
468
690
  // Auto-remember important things from the conversation
469
691
  await this.autoRemember(message, response);
692
+ void this.autoCaptureContext(message);
470
693
  // Log significant actions to hub
471
694
  if (response.length > 100) {
472
695
  const action = message.length > 50 ? message.slice(0, 50) + '...' : message;
@@ -478,16 +701,239 @@ ${hubContextStr}
478
701
  const errorMessage = error instanceof Error ? error.message : String(error);
479
702
  return `Error: ${errorMessage}`;
480
703
  }
704
+ finally {
705
+ this.attachmentContext = null;
706
+ }
707
+ }
708
+ tryHandleCalendarTodo(message) {
709
+ const todoItems = this.extractTodoItems(message);
710
+ const calendarEvents = this.extractCalendarEvents(message);
711
+ if (todoItems.length === 0 && calendarEvents.length === 0)
712
+ return null;
713
+ const results = [];
714
+ if (todoItems.length > 0) {
715
+ const todoRes = this.calendarTools.upsertTodoItems(todoItems);
716
+ if (!todoRes.success) {
717
+ return `Failed to update /todo: ${todoRes.error || todoRes.output || 'unknown error'}`;
718
+ }
719
+ results.push(`Updated /todo with ${todoItems.length} item(s).`);
720
+ }
721
+ if (calendarEvents.length > 0) {
722
+ const calRes = this.calendarTools.upsertCalendarEvents(calendarEvents);
723
+ if (!calRes.success) {
724
+ return `Failed to update /cal: ${calRes.error || calRes.output || 'unknown error'}`;
725
+ }
726
+ results.push(`Updated /cal with ${calendarEvents.length} event(s).`);
727
+ }
728
+ return `${results.join(' ')} Open /todo or /cal to view.`;
729
+ }
730
+ extractTodoItems(message) {
731
+ const lower = message.toLowerCase();
732
+ 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)) &&
733
+ /\b(add|put|save|store|track)\b/i.test(message);
734
+ if (!hasTodoIntent)
735
+ return [];
736
+ const items = [];
737
+ const wantSteps = /\b(step\s*[- ]?by\s*[- ]?step|break\s*down|smaller\s*steps|parse\s*it)\b/i.test(message);
738
+ const hasRag = /\b(rag|retrieval augmented|retrieval-augmented|embedding|embeddings|pierce)\b/i.test(message);
739
+ const hasWhenIsGood = /\b(whenisgood|when\s+is\s+good|availability|unavailable|schedule|scheduling|lab\s+meeting)\b/i.test(message);
740
+ if (wantSteps && hasRag) {
741
+ 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' });
742
+ }
743
+ if (wantSteps && hasWhenIsGood) {
744
+ 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' });
745
+ }
746
+ if (!wantSteps || (items.length === 0 && (hasRag || hasWhenIsGood))) {
747
+ const raw = message.trim().slice(0, 240);
748
+ items.push({
749
+ title: `[Todo] ${raw}${message.trim().length > 240 ? '…' : ''}`,
750
+ priority: 'medium',
751
+ notes: message.trim()
752
+ });
753
+ }
754
+ return items;
755
+ }
756
+ extractCalendarEvents(message) {
757
+ const hasCalendarIntent = /\bto\s+my\s+cal\b/i.test(message) ||
758
+ /\bto\s+my\s+calendar\b/i.test(message) ||
759
+ /(\s|^)\/cal\b/i.test(message) ||
760
+ /(\s|^)\/calendar\b/i.test(message);
761
+ const hasAddIntent = /\b(add|put|schedule|book)\b/i.test(message);
762
+ if (!hasAddIntent)
763
+ return [];
764
+ const candidates = message
765
+ .split(/\n\s*\n/)
766
+ .map((chunk) => chunk.trim())
767
+ .filter(Boolean);
768
+ const events = [];
769
+ for (const chunk of candidates) {
770
+ const parsed = this.parseCalendarEvent(chunk, hasCalendarIntent);
771
+ if (parsed)
772
+ events.push(parsed);
773
+ }
774
+ return events;
775
+ }
776
+ parseCalendarEvent(message, hasCalendarIntent) {
777
+ const explicit = this.parseExplicitDateTimeEvent(message, hasCalendarIntent);
778
+ if (explicit)
779
+ return explicit;
780
+ const weekdayMatch = message.match(/\b(mon(?:day)?|tue(?:sday)?|wed(?:nesday)?|thu(?:rsday)?|fri(?:day)?|sat(?:urday)?|sun(?:day)?)\b/i);
781
+ 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);
782
+ if (!weekdayMatch || !timeMatch)
783
+ return null;
784
+ const weekday = weekdayMatch[1].slice(0, 3).toLowerCase();
785
+ const weekdayIndex = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 }[weekday];
786
+ const startTime = this.to24h(timeMatch[1], timeMatch[2], timeMatch[3]);
787
+ const endTime = this.to24h(timeMatch[4], timeMatch[5], timeMatch[6]);
788
+ const startDate = this.nextOccurrence(weekdayIndex, startTime);
789
+ const dateKey = this.toDateKey(startDate);
790
+ const title = message
791
+ .replace(/^\s*add\s+/i, '')
792
+ .replace(/\s+to\s+\b(mon(?:day)?|tue(?:sday)?|wed(?:nesday)?|thu(?:rsday)?|fri(?:day)?|sat(?:urday)?|sun(?:day)?)\b[\s\S]*$/i, '')
793
+ .trim();
794
+ if (!title)
795
+ return null;
796
+ return {
797
+ title,
798
+ date: dateKey,
799
+ startTime,
800
+ endTime,
801
+ calendarId: 'primary'
802
+ };
803
+ }
804
+ parseExplicitDateTimeEvent(message, hasCalendarIntent) {
805
+ if (!hasCalendarIntent)
806
+ return null;
807
+ // e.g. "Tuesday, January 13, 3:30 pm, A211 PDB—TITLE, \"Talk...\" ... Host: ..."
808
+ 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);
809
+ if (!monthMatch)
810
+ return null;
811
+ const monthText = monthMatch[1].slice(0, 3).toLowerCase();
812
+ 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];
813
+ const dayMatch = message.match(new RegExp(`${monthMatch[1]}\\s+(\\d{1,2})`, 'i'));
814
+ const timeMatch = message.match(/\b(\d{1,2})(?::(\d{2}))?\s*(a|am|p|pm)\b/i);
815
+ if (!dayMatch || !timeMatch)
816
+ return null;
817
+ const dayOfMonth = Number(dayMatch[1]);
818
+ const startTime = this.to24h(timeMatch[1], timeMatch[2], timeMatch[3]);
819
+ const yearMatch = message.match(/\b(20\d{2})\b/);
820
+ const now = new Date();
821
+ const year = yearMatch ? Number(yearMatch[1]) : now.getFullYear();
822
+ const date = new Date(year, monthIndex, dayOfMonth, 0, 0, 0, 0);
823
+ const dateKey = this.toDateKey(date);
824
+ const cleaned = message
825
+ .replace(/^\s*add\s+(these\s+)?to\s+my\s+(cal|calendar)\b[:\s-]*/i, '')
826
+ .trim();
827
+ const parts = cleaned.split(/—|--|–/);
828
+ const head = parts[0] || cleaned;
829
+ const after = parts.slice(1).join('—').trim();
830
+ // Try to extract a succinct title from the segment after the location delimiter, otherwise use head.
831
+ const titleCandidate = after ? after : head;
832
+ const title = titleCandidate
833
+ .replace(/^[^A-Za-z0-9]*\b(mon(?:day)?|tue(?:sday)?|wed(?:nesday)?|thu(?:rsday)?|fri(?:day)?|sat(?:urday)?|sun(?:day)?)\b[, ]*/i, '')
834
+ .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, '')
835
+ .replace(/\b\d{1,2}(?::\d{2})?\s*(a|am|p|pm)\b/i, '')
836
+ .replace(/^[,\s]+|[,\s]+$/g, '')
837
+ .slice(0, 160);
838
+ if (!title)
839
+ return null;
840
+ const notes = cleaned.length > title.length ? cleaned : undefined;
841
+ return {
842
+ title,
843
+ date: dateKey,
844
+ startTime,
845
+ calendarId: 'primary',
846
+ notes
847
+ };
848
+ }
849
+ to24h(hourText, minuteText, meridiemText) {
850
+ const hour = Number(hourText);
851
+ const minute = Number(minuteText || '0');
852
+ const meridiem = meridiemText.toLowerCase();
853
+ let h = hour % 12;
854
+ if (meridiem.startsWith('p'))
855
+ h += 12;
856
+ return `${String(h).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
857
+ }
858
+ nextOccurrence(weekdayIndex, startTime) {
859
+ const now = new Date();
860
+ const candidate = new Date(now);
861
+ candidate.setHours(0, 0, 0, 0);
862
+ const delta = (weekdayIndex - candidate.getDay() + 7) % 7;
863
+ candidate.setDate(candidate.getDate() + delta);
864
+ if (startTime) {
865
+ const [h, m] = startTime.split(':').map(Number);
866
+ candidate.setHours(h, m, 0, 0);
867
+ if (candidate.getTime() <= now.getTime()) {
868
+ candidate.setDate(candidate.getDate() + 7);
869
+ }
870
+ }
871
+ return candidate;
872
+ }
873
+ toDateKey(date) {
874
+ const year = date.getFullYear();
875
+ const month = `${date.getMonth() + 1}`.padStart(2, '0');
876
+ const day = `${date.getDate()}`.padStart(2, '0');
877
+ return `${year}-${month}-${day}`;
481
878
  }
482
879
  /**
483
880
  * Get relevant memory context for a message
881
+ * Routes based on instanceMode:
882
+ * - personal: search personal memories + all team memories
883
+ * - team: search ONLY team memory (never personal - team Lia is isolated)
484
884
  */
485
885
  async getMemoryContext(message) {
486
886
  try {
887
+ const sections = [];
888
+ // Team mode: ONLY search team memory - never personal
889
+ if (this.instanceMode === 'team') {
890
+ if (!this.orgMemory) {
891
+ return null;
892
+ }
893
+ const teamMemories = await this.orgMemory.search(message, { limit: 10 });
894
+ const teamMemoryLines = teamMemories.map(m => `[team:${m.type}] ${m.content}`).join('\n');
895
+ const teamContextSummary = this.orgContextGraph
896
+ ? await this.orgContextGraph.getContextSummary(message, { limit: 5 })
897
+ : null;
898
+ if (teamMemoryLines)
899
+ sections.push(`TEAM MEMORIES:\n${teamMemoryLines}`);
900
+ if (teamContextSummary)
901
+ sections.push(`TEAM CONTEXT:\n${teamContextSummary}`);
902
+ if (sections.length === 0)
903
+ return null;
904
+ return sections.join('\n\n');
905
+ }
906
+ // Personal mode: search personal + all team memories
487
907
  const memories = await this.memory.search(message, { limit: 5 });
488
- if (memories.length === 0)
908
+ const memoryLines = memories.map(m => `[${m.type}] ${m.content}`).join('\n');
909
+ const contextSummary = await this.contextGraph.getContextSummary(message, { limit: 3 });
910
+ if (memoryLines)
911
+ sections.push(`YOUR MEMORIES:\n${memoryLines}`);
912
+ if (contextSummary)
913
+ sections.push(`YOUR CONTEXT:\n${contextSummary}`);
914
+ // In personal mode, also include all team memories for reading
915
+ for (const [orgId, orgMem] of this.userOrgMemories) {
916
+ try {
917
+ const teamMems = await orgMem.search(message, { limit: 3 });
918
+ if (teamMems.length > 0) {
919
+ const teamLines = teamMems.map(m => `[${m.type}] ${m.content}`).join('\n');
920
+ sections.push(`TEAM (${orgId}) MEMORIES:\n${teamLines}`);
921
+ }
922
+ const teamGraph = this.userOrgContextGraphs.get(orgId);
923
+ if (teamGraph) {
924
+ const teamContext = await teamGraph.getContextSummary(message, { limit: 2 });
925
+ if (teamContext) {
926
+ sections.push(`TEAM (${orgId}) CONTEXT:\n${teamContext}`);
927
+ }
928
+ }
929
+ }
930
+ catch {
931
+ // Skip failed team memory access
932
+ }
933
+ }
934
+ if (sections.length === 0)
489
935
  return null;
490
- return memories.map(m => `[${m.type}] ${m.content}`).join('\n');
936
+ return sections.join('\n\n');
491
937
  }
492
938
  catch {
493
939
  return null;
@@ -495,6 +941,9 @@ ${hubContextStr}
495
941
  }
496
942
  /**
497
943
  * Auto-remember important information from conversations
944
+ * Routes based on instanceMode:
945
+ * - personal: write to personal memory
946
+ * - team: write to team memory
498
947
  */
499
948
  async autoRemember(userMessage, response) {
500
949
  // Look for preference indicators
@@ -502,16 +951,140 @@ ${hubContextStr}
502
951
  const lower = userMessage.toLowerCase();
503
952
  for (const indicator of prefIndicators) {
504
953
  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
- });
954
+ // Route to correct memory store based on instanceMode
955
+ const targetMemory = this.instanceMode === 'team' ? this.orgMemory : this.memory;
956
+ if (targetMemory) {
957
+ await targetMemory.remember(userMessage, {
958
+ type: 'semantic',
959
+ source: 'auto-extract',
960
+ importance: 0.7,
961
+ tags: ['preference', this.instanceMode === 'team' ? 'team' : 'user']
962
+ });
963
+ }
511
964
  break;
512
965
  }
513
966
  }
514
967
  }
968
+ async autoCaptureContext(userMessage) {
969
+ const marker = '📎 Attached files:';
970
+ const base = userMessage.split(marker)[0]?.trim() || '';
971
+ if (!base || base.startsWith('/'))
972
+ return;
973
+ const text = base.replace(/\s+/g, ' ').trim();
974
+ const shareToOrg = !!this.orgContextGraph &&
975
+ /\b(share with team|team files|shared with team|team-wide|org-wide|company-wide)\b/i.test(text);
976
+ const graphs = shareToOrg && this.orgContextGraph
977
+ ? [this.contextGraph, this.orgContextGraph]
978
+ : [this.contextGraph];
979
+ const seenEntities = new Set();
980
+ const tasks = [];
981
+ const rememberEntity = (kind, name, attributes) => {
982
+ const cleaned = this.normalizeEntityName(name);
983
+ if (!cleaned || this.isIgnoredEntity(cleaned))
984
+ return;
985
+ for (const graph of graphs) {
986
+ const scopeLabel = graph === this.orgContextGraph ? 'org' : 'user';
987
+ const key = `${scopeLabel}:${kind}:${cleaned.toLowerCase()}`;
988
+ if (seenEntities.has(key))
989
+ continue;
990
+ seenEntities.add(key);
991
+ tasks.push(graph.upsertEntity({
992
+ kind,
993
+ name: cleaned,
994
+ attributes,
995
+ tags: ['ctx:auto']
996
+ }));
997
+ }
998
+ };
999
+ const rememberRelation = (from, to, type, fromKind, toKind) => {
1000
+ const fromClean = this.normalizeEntityName(from);
1001
+ const toClean = this.normalizeEntityName(to);
1002
+ if (!fromClean || !toClean || this.isIgnoredEntity(fromClean) || this.isIgnoredEntity(toClean))
1003
+ return;
1004
+ for (const graph of graphs) {
1005
+ tasks.push(graph.linkEntities({
1006
+ from: fromClean,
1007
+ to: toClean,
1008
+ type,
1009
+ fromKind,
1010
+ toKind,
1011
+ source: 'auto-capture'
1012
+ }));
1013
+ }
1014
+ };
1015
+ const emailMatches = text.match(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi) || [];
1016
+ for (const email of emailMatches.slice(0, 5)) {
1017
+ rememberEntity('person', email, { email });
1018
+ }
1019
+ for (const match of text.matchAll(/\bproject\s+([A-Za-z0-9][\w .'-]{1,60})/gi)) {
1020
+ rememberEntity('project', match[1]);
1021
+ }
1022
+ for (const match of text.matchAll(/\bteam\s+([A-Za-z0-9][\w .'-]{1,50})/gi)) {
1023
+ rememberEntity('team', match[1]);
1024
+ }
1025
+ for (const match of text.matchAll(/\b([A-Za-z0-9][\w .'-]{1,50})\s+team\b/gi)) {
1026
+ rememberEntity('team', match[1]);
1027
+ }
1028
+ for (const match of text.matchAll(/\b(company|org|organization)\s+([A-Za-z0-9][\w .'-]{1,60})/gi)) {
1029
+ rememberEntity('org', match[2]);
1030
+ }
1031
+ for (const match of text.matchAll(/\b(tool|platform|stack|using)\s+([A-Za-z0-9][\w .'-]{1,40})/gi)) {
1032
+ rememberEntity('tool', match[2]);
1033
+ }
1034
+ 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)) {
1035
+ rememberRelation(match[1], match[3], 'works_on', 'person', 'project');
1036
+ }
1037
+ 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)) {
1038
+ rememberRelation(match[1], match[2], 'owns', 'person', 'project');
1039
+ }
1040
+ 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)) {
1041
+ rememberRelation(match[1], match[2], 'leads', 'person', 'team');
1042
+ }
1043
+ 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)) {
1044
+ rememberRelation(match[1], match[2], 'member_of', 'person', 'team');
1045
+ }
1046
+ 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)) {
1047
+ rememberRelation(match[1], match[2], 'depends_on', 'project', 'project');
1048
+ }
1049
+ for (const match of text.matchAll(/([A-Za-z][\w .'-]{1,50})\s+reports to\s+([A-Za-z0-9][\w .'-]{1,50})/gi)) {
1050
+ rememberRelation(match[1], match[2], 'reports_to', 'person', 'person');
1051
+ }
1052
+ for (const match of text.matchAll(/([A-Za-z][\w .'-]{1,50})\s+uses\s+([A-Za-z0-9][\w .'-]{1,40})/gi)) {
1053
+ const subject = match[1];
1054
+ const tool = match[2];
1055
+ const lowered = subject.toLowerCase();
1056
+ let subjectKind = 'person';
1057
+ let subjectName = subject;
1058
+ if (lowered.includes('team')) {
1059
+ subjectKind = 'team';
1060
+ subjectName = subject.replace(/team/gi, '').trim();
1061
+ }
1062
+ else if (lowered.includes('project')) {
1063
+ subjectKind = 'project';
1064
+ subjectName = subject.replace(/project/gi, '').trim();
1065
+ }
1066
+ rememberEntity('tool', tool);
1067
+ rememberRelation(subjectName, tool, 'uses', subjectKind, 'tool');
1068
+ }
1069
+ if (tasks.length > 0) {
1070
+ try {
1071
+ await Promise.all(tasks);
1072
+ }
1073
+ catch {
1074
+ // Best-effort; context capture should never block responses.
1075
+ }
1076
+ }
1077
+ }
1078
+ normalizeEntityName(name) {
1079
+ return name.replace(/["'`]/g, '').replace(/\s+/g, ' ').trim();
1080
+ }
1081
+ isIgnoredEntity(name) {
1082
+ if (!name || name.length < 2 || name.length > 80)
1083
+ return true;
1084
+ const lower = name.toLowerCase();
1085
+ const ignored = new Set(['i', 'we', 'you', 'they', 'he', 'she', 'it', 'our', 'my', 'your', 'their']);
1086
+ return ignored.has(lower);
1087
+ }
515
1088
  /**
516
1089
  * Run the agentic loop
517
1090
  * @param apiKey Optional per-request API key (creates temporary client)
@@ -521,17 +1094,28 @@ ${hubContextStr}
521
1094
  let currentMessages = [...messages];
522
1095
  let iterations = 0;
523
1096
  const maxIterations = 30; // Increased for complex multi-tool tasks
1097
+ const requestTimeoutMs = 60000;
524
1098
  // Use per-request client if apiKey provided, otherwise use default
525
1099
  const client = apiKey ? new Anthropic({ apiKey }) : this.client;
526
1100
  while (iterations < maxIterations) {
527
1101
  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
- });
1102
+ const requestStart = Date.now();
1103
+ console.log(`[ORCHESTRATOR] Anthropic request ${iterations}/${maxIterations} (model: ${this.model})`);
1104
+ const response = await Promise.race([
1105
+ client.messages.create({
1106
+ model: this.model,
1107
+ max_tokens: this.maxTokens,
1108
+ system,
1109
+ tools,
1110
+ messages: currentMessages
1111
+ }),
1112
+ new Promise((_, reject) => {
1113
+ setTimeout(() => {
1114
+ reject(new Error(`Anthropic request timed out after ${requestTimeoutMs}ms`));
1115
+ }, requestTimeoutMs);
1116
+ })
1117
+ ]);
1118
+ console.log(`[ORCHESTRATOR] Anthropic response ${iterations}/${maxIterations} in ${Date.now() - requestStart}ms`);
535
1119
  // Check if done
536
1120
  if (response.stop_reason === 'end_turn' || !this.hasToolUse(response.content)) {
537
1121
  return this.extractText(response.content);
@@ -576,6 +1160,87 @@ For ANY file creation, editing, or system modification, use delegate_to_worker i
576
1160
  required: ['command']
577
1161
  }
578
1162
  },
1163
+ {
1164
+ name: 'calendar_upsert_events',
1165
+ description: `Create or update calendar events for /cal.
1166
+
1167
+ Writes ONLY to ~/Lia-Hub/shared/outputs/.lia/calendar.json (creates the directory/file if missing).`,
1168
+ input_schema: {
1169
+ type: 'object',
1170
+ properties: {
1171
+ events: {
1172
+ type: 'array',
1173
+ items: {
1174
+ type: 'object',
1175
+ properties: {
1176
+ id: { type: 'string' },
1177
+ title: { type: 'string' },
1178
+ date: { type: 'string', description: 'YYYY-MM-DD' },
1179
+ startTime: { type: 'string', description: 'HH:MM (24h) or ISO timestamp' },
1180
+ endTime: { type: 'string', description: 'HH:MM (24h) or ISO timestamp' },
1181
+ calendarId: { type: 'string' },
1182
+ notes: { type: 'string' }
1183
+ },
1184
+ required: ['title']
1185
+ }
1186
+ },
1187
+ calendars: {
1188
+ type: 'array',
1189
+ items: {
1190
+ type: 'object',
1191
+ properties: {
1192
+ id: { type: 'string' },
1193
+ name: { type: 'string' },
1194
+ color: { type: 'string' }
1195
+ },
1196
+ required: ['id', 'name']
1197
+ },
1198
+ description: 'Optional calendar definitions; omitted to keep existing.'
1199
+ }
1200
+ },
1201
+ required: ['events']
1202
+ }
1203
+ },
1204
+ {
1205
+ name: 'todo_upsert_items',
1206
+ description: `Create or update todo items for /todo.
1207
+
1208
+ Writes ONLY to ~/Lia-Hub/shared/outputs/.lia/todos.json (creates the directory/file if missing).`,
1209
+ input_schema: {
1210
+ type: 'object',
1211
+ properties: {
1212
+ items: {
1213
+ type: 'array',
1214
+ items: {
1215
+ type: 'object',
1216
+ properties: {
1217
+ id: { type: 'string' },
1218
+ title: { type: 'string' },
1219
+ done: { type: 'boolean' },
1220
+ due: { type: 'string' },
1221
+ priority: { type: 'string', enum: ['low', 'medium', 'high'] },
1222
+ notes: { type: 'string' },
1223
+ subtasks: {
1224
+ type: 'array',
1225
+ items: {
1226
+ type: 'object',
1227
+ properties: {
1228
+ id: { type: 'string' },
1229
+ title: { type: 'string' },
1230
+ done: { type: 'boolean' },
1231
+ notes: { type: 'string' }
1232
+ },
1233
+ required: ['title']
1234
+ }
1235
+ }
1236
+ },
1237
+ required: ['title']
1238
+ }
1239
+ }
1240
+ },
1241
+ required: ['items']
1242
+ }
1243
+ },
579
1244
  // Web tools
580
1245
  {
581
1246
  name: 'web_search',
@@ -759,6 +1424,110 @@ Be specific about what you want done.`,
759
1424
  required: ['content']
760
1425
  }
761
1426
  },
1427
+ {
1428
+ name: 'remember_entity',
1429
+ description: 'Store or update a team context entity (person, team, project, tool, task, doc).',
1430
+ input_schema: {
1431
+ type: 'object',
1432
+ properties: {
1433
+ kind: {
1434
+ type: 'string',
1435
+ enum: ContextGraphKinds.entities,
1436
+ description: 'Entity kind'
1437
+ },
1438
+ name: {
1439
+ type: 'string',
1440
+ description: 'Entity name'
1441
+ },
1442
+ description: {
1443
+ type: 'string',
1444
+ description: 'Short description or role'
1445
+ },
1446
+ attributes: {
1447
+ type: 'object',
1448
+ description: 'Optional key/value attributes',
1449
+ additionalProperties: { type: 'string' }
1450
+ },
1451
+ tags: {
1452
+ type: 'array',
1453
+ items: { type: 'string' },
1454
+ description: 'Optional tags'
1455
+ },
1456
+ scope: {
1457
+ type: 'string',
1458
+ enum: ['user', 'org'],
1459
+ description: 'Where to store the entity (default: user)'
1460
+ }
1461
+ },
1462
+ required: ['kind', 'name']
1463
+ }
1464
+ },
1465
+ {
1466
+ name: 'remember_relation',
1467
+ description: 'Link two entities with a relationship (depends_on, works_on, member_of, etc).',
1468
+ input_schema: {
1469
+ type: 'object',
1470
+ properties: {
1471
+ from: {
1472
+ type: 'string',
1473
+ description: 'Entity name or key'
1474
+ },
1475
+ to: {
1476
+ type: 'string',
1477
+ description: 'Entity name or key'
1478
+ },
1479
+ type: {
1480
+ type: 'string',
1481
+ enum: ContextGraphKinds.relations,
1482
+ description: 'Relationship type'
1483
+ },
1484
+ from_kind: {
1485
+ type: 'string',
1486
+ enum: ContextGraphKinds.entities,
1487
+ description: 'Optional kind hint for from'
1488
+ },
1489
+ to_kind: {
1490
+ type: 'string',
1491
+ enum: ContextGraphKinds.entities,
1492
+ description: 'Optional kind hint for to'
1493
+ },
1494
+ description: {
1495
+ type: 'string',
1496
+ description: 'Optional relation detail'
1497
+ },
1498
+ scope: {
1499
+ type: 'string',
1500
+ enum: ['user', 'org'],
1501
+ description: 'Where to store the relation (default: user)'
1502
+ }
1503
+ },
1504
+ required: ['from', 'to', 'type']
1505
+ }
1506
+ },
1507
+ {
1508
+ name: 'search_context',
1509
+ description: 'Search the context graph for entities and relationships.',
1510
+ input_schema: {
1511
+ type: 'object',
1512
+ properties: {
1513
+ query: {
1514
+ type: 'string',
1515
+ description: 'What to search for'
1516
+ },
1517
+ kind: {
1518
+ type: 'string',
1519
+ enum: ContextGraphKinds.entities,
1520
+ description: 'Optional entity kind filter'
1521
+ },
1522
+ scope: {
1523
+ type: 'string',
1524
+ enum: ['user', 'org'],
1525
+ description: 'Which context scope to search (default: user)'
1526
+ }
1527
+ },
1528
+ required: ['query']
1529
+ }
1530
+ },
762
1531
  {
763
1532
  name: 'search_memory',
764
1533
  description: 'Search your memory for relevant information. Use before delegating to include user context.',
@@ -859,6 +1628,54 @@ Be specific about what you want done.`,
859
1628
  },
860
1629
  required: ['port']
861
1630
  }
1631
+ },
1632
+ // Lia's internal task management tools
1633
+ {
1634
+ name: 'lia_plan',
1635
+ description: `Create a plan to break down a complex goal into steps. Use this when you receive multiple requests or need to organize work. The plan becomes your internal todo list.`,
1636
+ input_schema: {
1637
+ type: 'object',
1638
+ properties: {
1639
+ goal: {
1640
+ type: 'string',
1641
+ description: 'The overall goal you are trying to accomplish'
1642
+ },
1643
+ steps: {
1644
+ type: 'array',
1645
+ items: { type: 'string' },
1646
+ description: 'List of steps to accomplish the goal'
1647
+ }
1648
+ },
1649
+ required: ['goal', 'steps']
1650
+ }
1651
+ },
1652
+ {
1653
+ name: 'lia_add_task',
1654
+ description: `Add a task to your internal queue. Use when the user sends a new request while you are busy, or when you identify follow-up work. Tasks are processed in priority order.`,
1655
+ input_schema: {
1656
+ type: 'object',
1657
+ properties: {
1658
+ task: {
1659
+ type: 'string',
1660
+ description: 'The task description'
1661
+ },
1662
+ priority: {
1663
+ type: 'string',
1664
+ enum: ['urgent', 'high', 'normal', 'low'],
1665
+ description: 'Task priority (default: normal)'
1666
+ }
1667
+ },
1668
+ required: ['task']
1669
+ }
1670
+ },
1671
+ {
1672
+ name: 'lia_get_queue',
1673
+ description: 'Get your current task queue status. Shows what you are working on and what is pending.',
1674
+ input_schema: {
1675
+ type: 'object',
1676
+ properties: {},
1677
+ required: []
1678
+ }
862
1679
  }
863
1680
  ];
864
1681
  }
@@ -882,6 +1699,12 @@ Be specific about what you want done.`,
882
1699
  case 'bash':
883
1700
  result = await this.bashTool.execute(input.command, input.timeout);
884
1701
  break;
1702
+ case 'calendar_upsert_events':
1703
+ result = this.calendarTools.upsertCalendarEvents(input.events, input.calendars);
1704
+ break;
1705
+ case 'todo_upsert_items':
1706
+ result = this.calendarTools.upsertTodoItems(input.items);
1707
+ break;
885
1708
  case 'web_search':
886
1709
  result = await this.webTools.webSearch(input.query, { numResults: input.num_results });
887
1710
  break;
@@ -892,12 +1715,14 @@ Be specific about what you want done.`,
892
1715
  });
893
1716
  break;
894
1717
  // Worker tools
895
- case 'spawn_worker':
896
- result = await this.workerTools.spawnWorker(input.task, {
1718
+ case 'spawn_worker': {
1719
+ const { task } = this.withAttachments(input.task);
1720
+ result = await this.workerTools.spawnWorker(task, {
897
1721
  timeout: input.timeout,
898
1722
  priority: input.priority
899
1723
  });
900
1724
  break;
1725
+ }
901
1726
  case 'check_worker':
902
1727
  result = await this.workerTools.checkWorker(input.job_id);
903
1728
  break;
@@ -911,12 +1736,68 @@ Be specific about what you want done.`,
911
1736
  result = await this.workerTools.cancelWorker(input.job_id);
912
1737
  break;
913
1738
  // Legacy delegate tool
914
- case 'delegate_to_worker':
915
- result = await this.delegateToWorker(input.task, input.context, input.working_directory);
1739
+ case 'delegate_to_worker': {
1740
+ const { task, context } = this.withAttachments(input.task, input.context);
1741
+ result = await this.delegateToWorker(task, context, input.working_directory);
916
1742
  break;
1743
+ }
917
1744
  case 'remember':
918
1745
  result = await this.executeRemember(input.content, input.type, input.importance);
919
1746
  break;
1747
+ case 'remember_entity': {
1748
+ const scope = input.scope === 'org' ? 'org' : 'user';
1749
+ const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
1750
+ if (!graph) {
1751
+ result = { success: false, output: 'Org context is not configured for this agent.' };
1752
+ break;
1753
+ }
1754
+ const entity = await graph.upsertEntity({
1755
+ kind: input.kind,
1756
+ name: input.name,
1757
+ description: input.description,
1758
+ attributes: input.attributes,
1759
+ tags: input.tags
1760
+ });
1761
+ result = { success: true, output: `Stored ${scope} entity: ${entity.kind} ${entity.name} (${entity.key})` };
1762
+ break;
1763
+ }
1764
+ case 'remember_relation': {
1765
+ const scope = input.scope === 'org' ? 'org' : 'user';
1766
+ const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
1767
+ if (!graph) {
1768
+ result = { success: false, output: 'Org context is not configured for this agent.' };
1769
+ break;
1770
+ }
1771
+ const relation = await graph.linkEntities({
1772
+ from: input.from,
1773
+ to: input.to,
1774
+ type: input.type,
1775
+ description: input.description,
1776
+ fromKind: input.from_kind,
1777
+ toKind: input.to_kind
1778
+ });
1779
+ result = { success: true, output: `Linked ${scope}: ${relation.from} ${relation.type} ${relation.to}` };
1780
+ break;
1781
+ }
1782
+ case 'search_context': {
1783
+ const scope = input.scope === 'org' ? 'org' : 'user';
1784
+ const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
1785
+ if (!graph) {
1786
+ result = { success: false, output: 'Org context is not configured for this agent.' };
1787
+ break;
1788
+ }
1789
+ const entities = await graph.searchEntities(input.query, { kind: input.kind, limit: 6 });
1790
+ if (entities.length === 0) {
1791
+ result = { success: true, output: 'No context entities found.' };
1792
+ break;
1793
+ }
1794
+ const lines = entities.map(entity => {
1795
+ const desc = entity.description ? ` - ${entity.description}` : '';
1796
+ return `[${entity.kind}] ${entity.name}${desc} (${entity.key})`;
1797
+ });
1798
+ result = { success: true, output: lines.join('\n') };
1799
+ break;
1800
+ }
920
1801
  case 'search_memory':
921
1802
  result = await this.executeSearchMemory(input.query, input.type);
922
1803
  break;
@@ -938,6 +1819,16 @@ Be specific about what you want done.`,
938
1819
  case 'stop_local_server':
939
1820
  result = await this.executeStopServer(input.port);
940
1821
  break;
1822
+ // Lia's internal task management tools
1823
+ case 'lia_plan':
1824
+ result = this.executeLiaPlan(input.goal, input.steps);
1825
+ break;
1826
+ case 'lia_add_task':
1827
+ result = this.executeLiaAddTask(input.task, input.priority);
1828
+ break;
1829
+ case 'lia_get_queue':
1830
+ result = this.executeLiaGetQueue();
1831
+ break;
941
1832
  default:
942
1833
  result = { success: false, output: `Unknown tool: ${toolUse.name}` };
943
1834
  }
@@ -999,10 +1890,31 @@ Be specific about what you want done.`,
999
1890
  };
1000
1891
  // Escape single quotes in prompt for shell safety
1001
1892
  const escapedPrompt = prompt.replace(/'/g, "'\\''");
1893
+ const claudeCmd = `${this.claudePath} -p '${escapedPrompt}' --output-format text --dangerously-skip-permissions`;
1894
+ const isRoot = process.getuid?.() === 0;
1895
+ const shellCmd = isRoot
1896
+ ? `runuser -u lia -m -- /bin/bash -l -c "${claudeCmd.replace(/"/g, '\\"')}"`
1897
+ : claudeCmd;
1898
+ let workerUser = process.env.USER || 'lia';
1899
+ let workerLogname = process.env.LOGNAME || workerUser;
1900
+ if (isRoot) {
1901
+ workerUser = 'lia';
1902
+ workerLogname = 'lia';
1903
+ }
1904
+ const spawnEnv = {
1905
+ ...process.env,
1906
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
1907
+ GH_TOKEN: process.env.GH_TOKEN,
1908
+ GITHUB_TOKEN: process.env.GITHUB_TOKEN,
1909
+ HOME: cwd,
1910
+ LIA_WORKSPACE: cwd,
1911
+ USER: workerUser,
1912
+ LOGNAME: workerLogname
1913
+ };
1002
1914
  // Spawn worker
1003
- const child = spawn('/bin/bash', ['-l', '-c', `claude -p '${escapedPrompt}' --dangerously-skip-permissions`], {
1915
+ const child = spawn('/bin/bash', ['-l', '-c', shellCmd], {
1004
1916
  cwd,
1005
- env: { ...process.env },
1917
+ env: spawnEnv,
1006
1918
  stdio: ['pipe', 'pipe', 'pipe']
1007
1919
  });
1008
1920
  job.process = child;
@@ -1024,12 +1936,18 @@ Be specific about what you want done.`,
1024
1936
  job.output += text;
1025
1937
  // Stream output to console
1026
1938
  process.stdout.write(`[${id}] ${text}`);
1939
+ if (this.workerLogCallback) {
1940
+ this.workerLogCallback(id, text, 'stdout');
1941
+ }
1027
1942
  });
1028
1943
  child.stderr?.on('data', (data) => {
1029
1944
  const text = data.toString();
1030
1945
  if (!text.includes('Checking') && !text.includes('Connected')) {
1031
1946
  job.output += text;
1032
1947
  }
1948
+ if (this.workerLogCallback) {
1949
+ this.workerLogCallback(id, text, 'stderr');
1950
+ }
1033
1951
  });
1034
1952
  child.on('close', async (code) => {
1035
1953
  clearTimeout(timeout);
@@ -1196,9 +2114,24 @@ Be specific about what you want done.`,
1196
2114
  }
1197
2115
  /**
1198
2116
  * Execute remember tool
2117
+ * Routes writes based on instanceMode:
2118
+ * - personal: write to personal memory only
2119
+ * - team: write to team memory only
1199
2120
  */
1200
2121
  async executeRemember(content, type, importance) {
1201
2122
  try {
2123
+ // Team mode: write to org memory only
2124
+ if (this.instanceMode === 'team') {
2125
+ if (!this.orgMemory) {
2126
+ return { success: false, output: 'Team memory not configured. Cannot store team memory.' };
2127
+ }
2128
+ const id = await this.orgMemory.remember(content, {
2129
+ type: type || undefined,
2130
+ importance
2131
+ });
2132
+ return { success: true, output: `Remembered in team memory (${id})` };
2133
+ }
2134
+ // Personal mode: write to personal memory only
1202
2135
  const id = await this.memory.remember(content, {
1203
2136
  type: type || undefined,
1204
2137
  importance
@@ -1211,17 +2144,53 @@ Be specific about what you want done.`,
1211
2144
  }
1212
2145
  /**
1213
2146
  * Execute search memory tool
2147
+ * Routes searches based on instanceMode:
2148
+ * - personal: search personal + all team memories
2149
+ * - team: search team memory only (never personal)
1214
2150
  */
1215
2151
  async executeSearchMemory(query, type) {
1216
2152
  try {
1217
- const memories = await this.memory.search(query, {
2153
+ const searchOpts = {
1218
2154
  type: type || undefined,
1219
2155
  limit: 10
1220
- });
1221
- if (memories.length === 0) {
2156
+ };
2157
+ // Team mode: search only team memory
2158
+ if (this.instanceMode === 'team') {
2159
+ if (!this.orgMemory) {
2160
+ return { success: false, output: 'Team memory not configured.' };
2161
+ }
2162
+ const memories = await this.orgMemory.search(query, searchOpts);
2163
+ if (memories.length === 0) {
2164
+ return { success: true, output: 'No relevant team memories found.' };
2165
+ }
2166
+ const output = memories.map(m => `[team:${m.type}] (${(m.importance * 100).toFixed(0)}%) ${m.content}`).join('\n\n');
2167
+ return { success: true, output };
2168
+ }
2169
+ // Personal mode: search personal + all team memories
2170
+ const allMemories = [];
2171
+ // Search personal memories
2172
+ const personalMemories = await this.memory.search(query, { ...searchOpts, limit: 5 });
2173
+ for (const m of personalMemories) {
2174
+ allMemories.push({ source: 'personal', memory: m });
2175
+ }
2176
+ // Search all team memories user belongs to
2177
+ for (const [orgId, orgMem] of this.userOrgMemories) {
2178
+ try {
2179
+ const teamMems = await orgMem.search(query, { ...searchOpts, limit: 3 });
2180
+ for (const m of teamMems) {
2181
+ allMemories.push({ source: `team:${orgId}`, memory: m });
2182
+ }
2183
+ }
2184
+ catch {
2185
+ // Skip failed team memory searches
2186
+ }
2187
+ }
2188
+ if (allMemories.length === 0) {
1222
2189
  return { success: true, output: 'No relevant memories found.' };
1223
2190
  }
1224
- const output = memories.map(m => `[${m.type}] (${(m.importance * 100).toFixed(0)}%) ${m.content}`).join('\n\n');
2191
+ // Sort by importance and format
2192
+ allMemories.sort((a, b) => b.memory.importance - a.memory.importance);
2193
+ const output = allMemories.map(({ source, memory: m }) => `[${source}:${m.type}] (${(m.importance * 100).toFixed(0)}%) ${m.content}`).join('\n\n');
1225
2194
  return { success: true, output };
1226
2195
  }
1227
2196
  catch (error) {
@@ -1363,6 +2332,66 @@ Be specific about what you want done.`,
1363
2332
  return { success: false, output: `No server running on port ${port}` };
1364
2333
  }
1365
2334
  }
2335
+ /**
2336
+ * Create a plan with steps (Lia's internal planning)
2337
+ */
2338
+ executeLiaPlan(goal, steps) {
2339
+ try {
2340
+ const plan = this.taskQueue.createPlan(goal, steps);
2341
+ console.log(`[ORCHESTRATOR] Created plan: ${plan.id} with ${steps.length} steps`);
2342
+ const stepsFormatted = steps.map((s, i) => `${i + 1}. ${s}`).join('\n');
2343
+ return {
2344
+ success: true,
2345
+ output: `Created plan "${goal}" with ${steps.length} steps:\n${stepsFormatted}\n\nPlan ID: ${plan.id}`
2346
+ };
2347
+ }
2348
+ catch (error) {
2349
+ return { success: false, output: `Failed to create plan: ${error}` };
2350
+ }
2351
+ }
2352
+ /**
2353
+ * Add a task to Lia's internal queue
2354
+ */
2355
+ executeLiaAddTask(task, priority) {
2356
+ try {
2357
+ const newTask = this.taskQueue.addTask({
2358
+ type: 'follow_up',
2359
+ content: task,
2360
+ priority: priority || 'normal',
2361
+ metadata: {
2362
+ userId: this.userId,
2363
+ context: this.instanceMode,
2364
+ orgId: this.orgId,
2365
+ },
2366
+ });
2367
+ console.log(`[ORCHESTRATOR] Added task to queue: ${newTask.id}`);
2368
+ const status = this.taskQueue.getStatus();
2369
+ return {
2370
+ success: true,
2371
+ output: `Added to my queue: "${task}" (priority: ${priority || 'normal'}). ${status.pendingCount} tasks pending.`
2372
+ };
2373
+ }
2374
+ catch (error) {
2375
+ return { success: false, output: `Failed to add task: ${error}` };
2376
+ }
2377
+ }
2378
+ /**
2379
+ * Get Lia's task queue status
2380
+ */
2381
+ executeLiaGetQueue() {
2382
+ try {
2383
+ const status = this.taskQueue.getStatus();
2384
+ const todoList = this.taskQueue.formatAsTodoList();
2385
+ let output = todoList;
2386
+ if (!status.isProcessing && status.pendingCount === 0) {
2387
+ output = 'My queue is empty - ready for new tasks.';
2388
+ }
2389
+ return { success: true, output };
2390
+ }
2391
+ catch (error) {
2392
+ return { success: false, output: `Failed to get queue: ${error}` };
2393
+ }
2394
+ }
1366
2395
  /**
1367
2396
  * Format tool preview for user
1368
2397
  */
@@ -1396,6 +2425,12 @@ Be specific about what you want done.`,
1396
2425
  return `Delegating: ${input.task.slice(0, 60)}...`;
1397
2426
  case 'remember':
1398
2427
  return `Remembering: ${input.content.slice(0, 50)}...`;
2428
+ case 'remember_entity':
2429
+ return `Remembering entity: ${input.name}`;
2430
+ case 'remember_relation':
2431
+ return `Linking context: ${input.from} -> ${input.type} -> ${input.to}`;
2432
+ case 'search_context':
2433
+ return `Searching context: ${input.query}`;
1399
2434
  case 'search_memory':
1400
2435
  return `Searching memory: ${input.query}`;
1401
2436
  case 'schedule_task':
@@ -1406,6 +2441,12 @@ Be specific about what you want done.`,
1406
2441
  return `Starting server on port ${input.port || 8080}...`;
1407
2442
  case 'stop_local_server':
1408
2443
  return `Stopping server on port ${input.port}...`;
2444
+ case 'lia_plan':
2445
+ return `Planning: ${input.goal}`;
2446
+ case 'lia_add_task':
2447
+ return `Queuing task: ${input.task.slice(0, 50)}...`;
2448
+ case 'lia_get_queue':
2449
+ return 'Checking my task queue...';
1409
2450
  default:
1410
2451
  return null;
1411
2452
  }
@@ -1438,5 +2479,24 @@ Be specific about what you want done.`,
1438
2479
  else {
1439
2480
  this.memory.close();
1440
2481
  }
2482
+ if (this.orgMemory) {
2483
+ if (this.orgMemory instanceof PostgresMemoryStore) {
2484
+ await this.orgMemory.close();
2485
+ }
2486
+ else {
2487
+ this.orgMemory.close();
2488
+ }
2489
+ }
2490
+ // Close all user org memory stores (for personal mode reading team memories)
2491
+ for (const [, orgMem] of this.userOrgMemories) {
2492
+ if (orgMem instanceof PostgresMemoryStore) {
2493
+ await orgMem.close();
2494
+ }
2495
+ else {
2496
+ orgMem.close();
2497
+ }
2498
+ }
2499
+ this.userOrgMemories.clear();
2500
+ this.userOrgContextGraphs.clear();
1441
2501
  }
1442
2502
  }