@siftd/connect-agent 0.2.46 → 0.2.48

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.
@@ -66,6 +66,7 @@ export declare class MasterOrchestrator {
66
66
  private workerLogCallback;
67
67
  private workerStatusInterval;
68
68
  private attachmentContext;
69
+ private lastUserMessage;
69
70
  private galleryCallback;
70
71
  private userId;
71
72
  private workspaceDir;
@@ -193,7 +194,13 @@ export declare class MasterOrchestrator {
193
194
  private stripTodoSnapshot;
194
195
  private hasTodoMutation;
195
196
  private hasCalendarMutation;
196
- private hasExplicitDate;
197
+ private hasFileMutation;
198
+ private getUserTagHint;
199
+ private getBreakdownTarget;
200
+ private ensureBreakdownOriginalDone;
201
+ private getLocalDateKey;
202
+ private getLocalTimeZone;
203
+ private getTodoCalSystemPrompt;
197
204
  private getToolChoice;
198
205
  private withAttachments;
199
206
  private updateFileScope;
@@ -157,7 +157,9 @@ CALENDAR + TODO (Lia-managed data):
157
157
  - Keep these files hidden; refer users to /cal or /todo in responses
158
158
  - If the user provides subtasks or a bullet list under one task, store them as subtasks (unless they explicitly ask to split into multiple items)
159
159
  - Preserve explicit due dates, times, and priorities exactly as given (YYYY-MM-DD, HH:MM)
160
+ - Preserve task titles verbatim (including bracketed tags like [lia-test-...], quoted titles, and casing). Do not strip tags or rewrite titles.
160
161
  - If the user explicitly requests /todo or /cal changes, you MUST call the corresponding tool in the same response
162
+ - Never delegate /todo or /cal changes to workers; use the tools directly in the same response
161
163
  - If the user says "Add to /todo:" followed by a list of deadlines/details, create ONE todo with notes unless they explicitly ask to split/break down
162
164
  - Schemas:
163
165
  - calendar.json: { "version": 1, "calendars": [...], "events": [...] }
@@ -185,6 +187,16 @@ Before complex work: Check CLAUDE.md → Read LANDMARKS.md → Search memory
185
187
  After completing work: Update LANDMARKS.md → Remember learnings
186
188
 
187
189
  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).`;
190
+ const TODO_CAL_SYSTEM_PROMPT_BASE = `You are Lia. Your ONLY job is to update /todo and /cal using tools.
191
+
192
+ Rules:
193
+ - If the user requests /todo changes, call todo_upsert_items.
194
+ - If the user requests /cal or calendar changes, call calendar_upsert_events.
195
+ - If both are requested, call BOTH tools in the same response.
196
+ - Preserve exact titles, dates, times, notes, and any bracketed tags (e.g., [lia-test-...]).
197
+ - Do not spawn workers or call any other tools.
198
+ - Do not ask follow-up questions; infer reasonable defaults from the request.
199
+ - After tool calls, respond briefly confirming the updates.`;
188
200
  export class MasterOrchestrator {
189
201
  client;
190
202
  model;
@@ -206,6 +218,7 @@ export class MasterOrchestrator {
206
218
  workerLogCallback = null;
207
219
  workerStatusInterval = null;
208
220
  attachmentContext = null;
221
+ lastUserMessage = null;
209
222
  galleryCallback = null;
210
223
  userId;
211
224
  workspaceDir;
@@ -688,8 +701,73 @@ export class MasterOrchestrator {
688
701
  const query = /\b(what|show|list|open|view|see)\b/.test(lower);
689
702
  return target && action && !query;
690
703
  }
691
- hasExplicitDate(message) {
692
- return /\b\d{4}-\d{2}-\d{2}\b/.test(this.stripTodoSnapshot(message));
704
+ hasFileMutation(message) {
705
+ const lower = this.stripTodoSnapshot(message).toLowerCase();
706
+ const target = /(^|\s)\/files\b|\bfiles?\b/.test(lower);
707
+ const action = /\b(create|make|write|save|generate|export|upload|attach|produce|edit|update|modify|delete|remove)\b/.test(lower);
708
+ const query = /\b(what|show|list|open|view|see|browse|find)\b/.test(lower);
709
+ return target && action && !query;
710
+ }
711
+ getUserTagHint(message) {
712
+ if (!message)
713
+ return null;
714
+ const base = this.stripTodoSnapshot(message);
715
+ const matches = base.match(/\[[^\]]{2,64}\]/g) || [];
716
+ const unique = Array.from(new Set(matches.map((tag) => tag.trim()))).filter(Boolean);
717
+ if (unique.length !== 1)
718
+ return null;
719
+ return unique[0];
720
+ }
721
+ getBreakdownTarget(message) {
722
+ if (!message)
723
+ return null;
724
+ const stripped = this.stripTodoSnapshot(message);
725
+ if (!/\b(break(?:\s|-)?down|split|parse)\b/i.test(stripped))
726
+ return null;
727
+ const quoted = stripped.match(/["“]([^"”]{3,160})["”]/);
728
+ if (quoted && quoted[1]) {
729
+ return quoted[1].trim();
730
+ }
731
+ const inline = stripped.match(/break(?:\s|-)?down\s+the\s+(.+?)\s+(?:item|todo|task)\b/i);
732
+ if (inline && inline[1]) {
733
+ return inline[1].trim();
734
+ }
735
+ return null;
736
+ }
737
+ ensureBreakdownOriginalDone(items, targetTitle) {
738
+ const normalizedTarget = targetTitle.trim().toLowerCase();
739
+ let found = false;
740
+ const nextItems = items.map((item) => {
741
+ if (item?.title && item.title.trim().toLowerCase() === normalizedTarget) {
742
+ found = true;
743
+ return { ...item, done: true };
744
+ }
745
+ return item;
746
+ });
747
+ if (!found) {
748
+ nextItems.unshift({ title: targetTitle, done: true });
749
+ }
750
+ return nextItems;
751
+ }
752
+ getLocalDateKey(date = new Date()) {
753
+ const year = date.getFullYear();
754
+ const month = `${date.getMonth() + 1}`.padStart(2, '0');
755
+ const day = `${date.getDate()}`.padStart(2, '0');
756
+ return `${year}-${month}-${day}`;
757
+ }
758
+ getLocalTimeZone() {
759
+ try {
760
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || null;
761
+ }
762
+ catch {
763
+ return null;
764
+ }
765
+ }
766
+ getTodoCalSystemPrompt() {
767
+ const today = this.getLocalDateKey();
768
+ const timeZone = this.getLocalTimeZone();
769
+ const tzNote = timeZone ? ` (${timeZone})` : '';
770
+ return `${TODO_CAL_SYSTEM_PROMPT_BASE}\n\nDate context:\n- Today is ${today}${tzNote}.\n- Use this to resolve relative dates like "today", "tomorrow", or "next week".`;
693
771
  }
694
772
  getToolChoice(messages) {
695
773
  const last = messages[messages.length - 1];
@@ -697,12 +775,16 @@ export class MasterOrchestrator {
697
775
  return undefined;
698
776
  const wantsTodo = this.hasTodoMutation(last.content);
699
777
  const wantsCal = this.hasCalendarMutation(last.content);
778
+ const wantsFiles = this.hasFileMutation(last.content);
700
779
  if (wantsTodo && !wantsCal) {
701
780
  return { type: 'tool', name: 'todo_upsert_items' };
702
781
  }
703
- if (wantsCal && !wantsTodo && this.hasExplicitDate(last.content)) {
782
+ if (wantsCal && !wantsTodo) {
704
783
  return { type: 'tool', name: 'calendar_upsert_events' };
705
784
  }
785
+ if (wantsFiles) {
786
+ return { type: 'tool', name: 'delegate_to_worker' };
787
+ }
706
788
  return undefined;
707
789
  }
708
790
  withAttachments(task, context) {
@@ -775,12 +857,28 @@ export class MasterOrchestrator {
775
857
  * Process a user message
776
858
  */
777
859
  async processMessage(message, conversationHistory = [], sendMessage) {
860
+ this.lastUserMessage = message;
778
861
  // Handle slash commands first
779
862
  const slashResponse = this.handleSlashCommand(message);
780
863
  if (slashResponse) {
781
864
  return slashResponse;
782
865
  }
783
866
  this.updateFileScope(message);
867
+ const wantsTodoOrCal = this.hasTodoMutation(message) || this.hasCalendarMutation(message);
868
+ if (wantsTodoOrCal) {
869
+ this.attachmentContext = this.extractAttachmentContext(message);
870
+ const toolMessages = [{ role: 'user', content: message }];
871
+ try {
872
+ return await this.runAgentLoop(toolMessages, this.getTodoCalSystemPrompt(), sendMessage);
873
+ }
874
+ catch (error) {
875
+ const errorMessage = error instanceof Error ? error.message : String(error);
876
+ return `Error: ${errorMessage}`;
877
+ }
878
+ finally {
879
+ this.attachmentContext = null;
880
+ }
881
+ }
784
882
  // DISABLED: Dumb regex extraction was creating garbage todos
785
883
  // Let the AI use calendar_upsert_events and todo_upsert_items tools properly
786
884
  // const quickWrite = this.tryHandleCalendarTodo(message);
@@ -1231,14 +1329,34 @@ ${hubContextStr}
1231
1329
  * Run the agentic loop
1232
1330
  */
1233
1331
  async runAgentLoop(messages, system, sendMessage) {
1234
- const tools = this.getToolDefinitions();
1332
+ const toolsBase = this.getToolDefinitions();
1333
+ const last = messages[messages.length - 1];
1334
+ const lastContent = last && last.role === 'user' && typeof last.content === 'string' ? last.content : '';
1335
+ const restrictWorkers = lastContent
1336
+ ? (this.hasTodoMutation(lastContent) || this.hasCalendarMutation(lastContent))
1337
+ : false;
1338
+ const wantsTodoOrCal = restrictWorkers;
1339
+ const todoCalTools = new Set(['calendar_upsert_events', 'todo_upsert_items']);
1340
+ const blockedTools = new Set([
1341
+ 'spawn_worker',
1342
+ 'delegate_to_worker',
1343
+ 'check_worker',
1344
+ 'wait_worker',
1345
+ 'list_workers',
1346
+ 'cancel_worker',
1347
+ ]);
1348
+ const tools = wantsTodoOrCal
1349
+ ? toolsBase.filter((tool) => todoCalTools.has(tool.name))
1350
+ : toolsBase.filter((tool) => !blockedTools.has(tool.name) || !restrictWorkers);
1235
1351
  let currentMessages = [...messages];
1236
1352
  let iterations = 0;
1237
1353
  const maxIterations = 30; // Increased for complex multi-tool tasks
1238
1354
  const requestTimeoutMs = 60000;
1355
+ const forcedToolChoice = this.getToolChoice(currentMessages);
1356
+ let retriedForcedTool = false;
1239
1357
  while (iterations < maxIterations) {
1240
1358
  iterations++;
1241
- const toolChoice = this.getToolChoice(currentMessages);
1359
+ const toolChoice = forcedToolChoice ?? this.getToolChoice(currentMessages);
1242
1360
  const requestStart = Date.now();
1243
1361
  console.log(`[ORCHESTRATOR] Anthropic request ${iterations}/${maxIterations} (model: ${this.model})`);
1244
1362
  const response = await Promise.race([
@@ -1259,10 +1377,37 @@ ${hubContextStr}
1259
1377
  console.log(`[ORCHESTRATOR] Anthropic response ${iterations}/${maxIterations} in ${Date.now() - requestStart}ms`);
1260
1378
  // Check if done
1261
1379
  if (response.stop_reason === 'end_turn' || !this.hasToolUse(response.content)) {
1380
+ if (forcedToolChoice && !retriedForcedTool) {
1381
+ retriedForcedTool = true;
1382
+ const toolName = forcedToolChoice.name;
1383
+ const needsNoWorkers = toolName === 'todo_upsert_items' || toolName === 'calendar_upsert_events';
1384
+ const followup = needsNoWorkers
1385
+ ? `You must call the ${toolName} tool now. Use the exact task/event titles and any bracketed tags exactly as provided. Do not spawn workers.`
1386
+ : `You must call the ${toolName} tool now. Use the user's request as the task details.`;
1387
+ currentMessages = [
1388
+ ...currentMessages,
1389
+ { role: 'assistant', content: response.content },
1390
+ {
1391
+ role: 'user',
1392
+ content: followup
1393
+ }
1394
+ ];
1395
+ continue;
1396
+ }
1262
1397
  return this.extractText(response.content);
1263
1398
  }
1399
+ const toolUseBlocks = response.content.filter((block) => block.type === 'tool_use');
1400
+ const toolNames = toolUseBlocks.map((block) => block.name);
1264
1401
  // Process tool calls
1265
1402
  const toolResults = await this.processToolCalls(response.content, sendMessage);
1403
+ if (wantsTodoOrCal && (toolNames.includes('todo_upsert_items') || toolNames.includes('calendar_upsert_events'))) {
1404
+ const updates = [];
1405
+ if (toolNames.includes('todo_upsert_items'))
1406
+ updates.push('Updated /todo.');
1407
+ if (toolNames.includes('calendar_upsert_events'))
1408
+ updates.push('Updated /cal.');
1409
+ return `${updates.join(' ')} Open /todo or /cal to view.`;
1410
+ }
1266
1411
  // Continue conversation
1267
1412
  currentMessages = [
1268
1413
  ...currentMessages,
@@ -1878,6 +2023,17 @@ Unlike lia_plan (internal only), this creates a VISIBLE todo list that appears i
1878
2023
  async processToolCalls(content, sendMessage) {
1879
2024
  const toolUseBlocks = content.filter((block) => block.type === 'tool_use');
1880
2025
  const results = [];
2026
+ const wantsTodoOrCal = this.lastUserMessage
2027
+ ? (this.hasTodoMutation(this.lastUserMessage) || this.hasCalendarMutation(this.lastUserMessage))
2028
+ : false;
2029
+ const blockedWorkerTools = new Set([
2030
+ 'spawn_worker',
2031
+ 'delegate_to_worker',
2032
+ 'check_worker',
2033
+ 'wait_worker',
2034
+ 'list_workers',
2035
+ 'cancel_worker',
2036
+ ]);
1881
2037
  for (const toolUse of toolUseBlocks) {
1882
2038
  const input = toolUse.input;
1883
2039
  let result;
@@ -1887,151 +2043,183 @@ Unlike lia_plan (internal only), this creates a VISIBLE todo list that appears i
1887
2043
  if (preview)
1888
2044
  await sendMessage(preview);
1889
2045
  }
1890
- switch (toolUse.name) {
1891
- // New direct tools
1892
- case 'bash':
1893
- result = await this.bashTool.execute(input.command, input.timeout);
1894
- break;
1895
- case 'calendar_upsert_events':
1896
- result = this.calendarTools.upsertCalendarEvents(input.events, input.calendars);
1897
- break;
1898
- case 'todo_upsert_items':
1899
- result = this.calendarTools.upsertTodoItems(input.items);
1900
- break;
1901
- case 'web_search':
1902
- result = await this.webTools.webSearch(input.query, { numResults: input.num_results });
1903
- break;
1904
- case 'fetch_url':
1905
- result = await this.webTools.fetchUrl(input.url, {
1906
- format: input.format,
1907
- timeout: input.timeout
1908
- });
1909
- break;
1910
- // Worker tools
1911
- case 'spawn_worker': {
1912
- const { task } = this.withAttachments(input.task);
1913
- const fileScope = this.getFileScopeOverrides();
1914
- const scopedTask = fileScope.instructions ? `${fileScope.instructions}\n\n${task}` : task;
1915
- result = await this.workerTools.spawnWorker(scopedTask, {
1916
- timeout: input.timeout,
1917
- priority: input.priority,
1918
- workingDirectory: input.working_directory || fileScope.workingDir
1919
- });
1920
- break;
1921
- }
1922
- case 'check_worker':
1923
- result = await this.workerTools.checkWorker(input.job_id);
1924
- break;
1925
- case 'wait_worker':
1926
- result = await this.workerTools.waitWorker(input.job_id, input.max_wait);
1927
- break;
1928
- case 'list_workers':
1929
- result = await this.workerTools.listWorkers(input.status);
1930
- break;
1931
- case 'cancel_worker':
1932
- result = await this.workerTools.cancelWorker(input.job_id);
1933
- break;
1934
- // Legacy delegate tool
1935
- case 'delegate_to_worker': {
1936
- const { task, context } = this.withAttachments(input.task, input.context);
1937
- const fileScope = this.getFileScopeOverrides();
1938
- result = await this.delegateToWorker(task, context, input.working_directory || fileScope.workingDir, fileScope.instructions);
1939
- break;
1940
- }
1941
- case 'remember':
1942
- result = await this.executeRemember(input.content, input.type, input.importance);
1943
- break;
1944
- case 'remember_entity': {
1945
- const scope = input.scope === 'org' ? 'org' : 'user';
1946
- const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
1947
- if (!graph) {
1948
- result = { success: false, output: 'Org context is not configured for this agent.' };
2046
+ if (wantsTodoOrCal && blockedWorkerTools.has(toolUse.name)) {
2047
+ result = {
2048
+ success: false,
2049
+ output: '',
2050
+ error: 'Do not use worker tools for /todo or /cal. Call todo_upsert_items or calendar_upsert_events directly.'
2051
+ };
2052
+ }
2053
+ else
2054
+ switch (toolUse.name) {
2055
+ // New direct tools
2056
+ case 'bash':
2057
+ result = await this.bashTool.execute(input.command, input.timeout);
2058
+ break;
2059
+ case 'calendar_upsert_events':
2060
+ {
2061
+ const tagHint = this.getUserTagHint(this.lastUserMessage);
2062
+ const events = input.events || [];
2063
+ const taggedEvents = tagHint
2064
+ ? events.map((event) => ({
2065
+ ...event,
2066
+ title: event.title.includes(tagHint) ? event.title : `${event.title} ${tagHint}`
2067
+ }))
2068
+ : events;
2069
+ result = this.calendarTools.upsertCalendarEvents(taggedEvents, input.calendars);
2070
+ }
2071
+ break;
2072
+ case 'todo_upsert_items':
2073
+ {
2074
+ const tagHint = this.getUserTagHint(this.lastUserMessage);
2075
+ let items = input.items || [];
2076
+ const breakdownTarget = this.getBreakdownTarget(this.lastUserMessage);
2077
+ if (breakdownTarget) {
2078
+ items = this.ensureBreakdownOriginalDone(items, breakdownTarget);
2079
+ }
2080
+ const taggedItems = tagHint
2081
+ ? items.map((item) => ({
2082
+ ...item,
2083
+ title: item.title.includes(tagHint) ? item.title : `${item.title} ${tagHint}`
2084
+ }))
2085
+ : items;
2086
+ result = this.calendarTools.upsertTodoItems(taggedItems);
2087
+ }
2088
+ break;
2089
+ case 'web_search':
2090
+ result = await this.webTools.webSearch(input.query, { numResults: input.num_results });
2091
+ break;
2092
+ case 'fetch_url':
2093
+ result = await this.webTools.fetchUrl(input.url, {
2094
+ format: input.format,
2095
+ timeout: input.timeout
2096
+ });
2097
+ break;
2098
+ // Worker tools
2099
+ case 'spawn_worker': {
2100
+ const { task } = this.withAttachments(input.task);
2101
+ const fileScope = this.getFileScopeOverrides();
2102
+ const scopedTask = fileScope.instructions ? `${fileScope.instructions}\n\n${task}` : task;
2103
+ result = await this.workerTools.spawnWorker(scopedTask, {
2104
+ timeout: input.timeout,
2105
+ priority: input.priority,
2106
+ workingDirectory: input.working_directory || fileScope.workingDir
2107
+ });
1949
2108
  break;
1950
2109
  }
1951
- const entity = await graph.upsertEntity({
1952
- kind: input.kind,
1953
- name: input.name,
1954
- description: input.description,
1955
- attributes: input.attributes,
1956
- tags: input.tags
1957
- });
1958
- result = { success: true, output: `Stored ${scope} entity: ${entity.kind} ${entity.name} (${entity.key})` };
1959
- break;
1960
- }
1961
- case 'remember_relation': {
1962
- const scope = input.scope === 'org' ? 'org' : 'user';
1963
- const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
1964
- if (!graph) {
1965
- result = { success: false, output: 'Org context is not configured for this agent.' };
2110
+ case 'check_worker':
2111
+ result = await this.workerTools.checkWorker(input.job_id);
2112
+ break;
2113
+ case 'wait_worker':
2114
+ result = await this.workerTools.waitWorker(input.job_id, input.max_wait);
2115
+ break;
2116
+ case 'list_workers':
2117
+ result = await this.workerTools.listWorkers(input.status);
2118
+ break;
2119
+ case 'cancel_worker':
2120
+ result = await this.workerTools.cancelWorker(input.job_id);
2121
+ break;
2122
+ // Legacy delegate tool
2123
+ case 'delegate_to_worker': {
2124
+ const { task, context } = this.withAttachments(input.task, input.context);
2125
+ const fileScope = this.getFileScopeOverrides();
2126
+ result = await this.delegateToWorker(task, context, input.working_directory || fileScope.workingDir, fileScope.instructions);
1966
2127
  break;
1967
2128
  }
1968
- const relation = await graph.linkEntities({
1969
- from: input.from,
1970
- to: input.to,
1971
- type: input.type,
1972
- description: input.description,
1973
- fromKind: input.from_kind,
1974
- toKind: input.to_kind
1975
- });
1976
- result = { success: true, output: `Linked ${scope}: ${relation.from} ${relation.type} ${relation.to}` };
1977
- break;
1978
- }
1979
- case 'search_context': {
1980
- const scope = input.scope === 'org' ? 'org' : 'user';
1981
- const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
1982
- if (!graph) {
1983
- result = { success: false, output: 'Org context is not configured for this agent.' };
2129
+ case 'remember':
2130
+ result = await this.executeRemember(input.content, input.type, input.importance);
2131
+ break;
2132
+ case 'remember_entity': {
2133
+ const scope = input.scope === 'org' ? 'org' : 'user';
2134
+ const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
2135
+ if (!graph) {
2136
+ result = { success: false, output: 'Org context is not configured for this agent.' };
2137
+ break;
2138
+ }
2139
+ const entity = await graph.upsertEntity({
2140
+ kind: input.kind,
2141
+ name: input.name,
2142
+ description: input.description,
2143
+ attributes: input.attributes,
2144
+ tags: input.tags
2145
+ });
2146
+ result = { success: true, output: `Stored ${scope} entity: ${entity.kind} ${entity.name} (${entity.key})` };
1984
2147
  break;
1985
2148
  }
1986
- const entities = await graph.searchEntities(input.query, { kind: input.kind, limit: 6 });
1987
- if (entities.length === 0) {
1988
- result = { success: true, output: 'No context entities found.' };
2149
+ case 'remember_relation': {
2150
+ const scope = input.scope === 'org' ? 'org' : 'user';
2151
+ const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
2152
+ if (!graph) {
2153
+ result = { success: false, output: 'Org context is not configured for this agent.' };
2154
+ break;
2155
+ }
2156
+ const relation = await graph.linkEntities({
2157
+ from: input.from,
2158
+ to: input.to,
2159
+ type: input.type,
2160
+ description: input.description,
2161
+ fromKind: input.from_kind,
2162
+ toKind: input.to_kind
2163
+ });
2164
+ result = { success: true, output: `Linked ${scope}: ${relation.from} ${relation.type} ${relation.to}` };
1989
2165
  break;
1990
2166
  }
1991
- const lines = entities.map(entity => {
1992
- const desc = entity.description ? ` - ${entity.description}` : '';
1993
- return `[${entity.kind}] ${entity.name}${desc} (${entity.key})`;
1994
- });
1995
- result = { success: true, output: lines.join('\n') };
1996
- break;
2167
+ case 'search_context': {
2168
+ const scope = input.scope === 'org' ? 'org' : 'user';
2169
+ const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
2170
+ if (!graph) {
2171
+ result = { success: false, output: 'Org context is not configured for this agent.' };
2172
+ break;
2173
+ }
2174
+ const entities = await graph.searchEntities(input.query, { kind: input.kind, limit: 6 });
2175
+ if (entities.length === 0) {
2176
+ result = { success: true, output: 'No context entities found.' };
2177
+ break;
2178
+ }
2179
+ const lines = entities.map(entity => {
2180
+ const desc = entity.description ? ` - ${entity.description}` : '';
2181
+ return `[${entity.kind}] ${entity.name}${desc} (${entity.key})`;
2182
+ });
2183
+ result = { success: true, output: lines.join('\n') };
2184
+ break;
2185
+ }
2186
+ case 'search_memory':
2187
+ result = await this.executeSearchMemory(input.query, input.type);
2188
+ break;
2189
+ case 'schedule_task':
2190
+ result = await this.executeScheduleTask(input.task, input.when);
2191
+ break;
2192
+ case 'list_scheduled':
2193
+ result = await this.executeListScheduled();
2194
+ break;
2195
+ case 'memory_stats':
2196
+ result = await this.executeMemoryStats();
2197
+ break;
2198
+ case 'open_browser':
2199
+ result = await this.executeOpenBrowser(input.url);
2200
+ break;
2201
+ case 'start_local_server':
2202
+ result = await this.executeStartServer(input.directory, input.port);
2203
+ break;
2204
+ case 'stop_local_server':
2205
+ result = await this.executeStopServer(input.port);
2206
+ break;
2207
+ // Lia's internal task management tools
2208
+ case 'lia_plan':
2209
+ result = this.executeLiaPlan(input.goal, input.steps);
2210
+ break;
2211
+ case 'lia_add_task':
2212
+ result = this.executeLiaAddTask(input.task, input.priority);
2213
+ break;
2214
+ case 'lia_get_queue':
2215
+ result = this.executeLiaGetQueue();
2216
+ break;
2217
+ case 'lia_todo_write':
2218
+ result = this.executeLiaTodoWrite(input.todos);
2219
+ break;
2220
+ default:
2221
+ result = { success: false, output: `Unknown tool: ${toolUse.name}` };
1997
2222
  }
1998
- case 'search_memory':
1999
- result = await this.executeSearchMemory(input.query, input.type);
2000
- break;
2001
- case 'schedule_task':
2002
- result = await this.executeScheduleTask(input.task, input.when);
2003
- break;
2004
- case 'list_scheduled':
2005
- result = await this.executeListScheduled();
2006
- break;
2007
- case 'memory_stats':
2008
- result = await this.executeMemoryStats();
2009
- break;
2010
- case 'open_browser':
2011
- result = await this.executeOpenBrowser(input.url);
2012
- break;
2013
- case 'start_local_server':
2014
- result = await this.executeStartServer(input.directory, input.port);
2015
- break;
2016
- case 'stop_local_server':
2017
- result = await this.executeStopServer(input.port);
2018
- break;
2019
- // Lia's internal task management tools
2020
- case 'lia_plan':
2021
- result = this.executeLiaPlan(input.goal, input.steps);
2022
- break;
2023
- case 'lia_add_task':
2024
- result = this.executeLiaAddTask(input.task, input.priority);
2025
- break;
2026
- case 'lia_get_queue':
2027
- result = this.executeLiaGetQueue();
2028
- break;
2029
- case 'lia_todo_write':
2030
- result = this.executeLiaTodoWrite(input.todos);
2031
- break;
2032
- default:
2033
- result = { success: false, output: `Unknown tool: ${toolUse.name}` };
2034
- }
2035
2223
  // Anthropic API requires content to be non-empty when is_error is true
2036
2224
  // Belt-and-suspenders: ensure content is NEVER empty
2037
2225
  let content = result.output || '';
@@ -8,6 +8,7 @@ export { GalleryCallback, GalleryWorker };
8
8
  export type { WorkerLogCallback };
9
9
  export declare class WorkerTools {
10
10
  private manager;
11
+ private workspaceDir;
11
12
  constructor(workspaceDir: string);
12
13
  /**
13
14
  * Set callback for gallery updates (worker assets for UI)
@@ -5,8 +5,10 @@
5
5
  import { WorkerManager } from '../workers/manager.js';
6
6
  export class WorkerTools {
7
7
  manager;
8
+ workspaceDir;
8
9
  constructor(workspaceDir) {
9
10
  this.manager = new WorkerManager(workspaceDir);
11
+ this.workspaceDir = workspaceDir;
10
12
  }
11
13
  /**
12
14
  * Set callback for gallery updates (worker assets for UI)
@@ -34,7 +36,7 @@ export class WorkerTools {
34
36
  const jobId = await this.manager.spawn(task, {
35
37
  timeout: options?.timeout,
36
38
  priority: options?.priority,
37
- workspace: options?.workingDirectory
39
+ workspace: options?.workingDirectory || this.workspaceDir
38
40
  });
39
41
  const job = this.manager.get(jobId);
40
42
  return {
@@ -11,7 +11,7 @@ import { AssetResponse } from './core/asset-api.js';
11
11
  export type MessageHandler = (message: WebSocketMessage) => Promise<void>;
12
12
  export type StreamHandler = (chunk: string) => void;
13
13
  export interface WebSocketMessage {
14
- type: 'message' | 'interrupt' | 'ping' | 'pong' | 'connected' | 'asset_request';
14
+ type: 'message' | 'interrupt' | 'ping' | 'pong' | 'connected' | 'asset_request' | 'ready';
15
15
  id?: string;
16
16
  content?: string;
17
17
  timestamp?: number;
@@ -37,6 +37,8 @@ export declare class AgentWebSocket {
37
37
  private isConnected;
38
38
  private pendingResponses;
39
39
  private cloudflareBlocked;
40
+ private pendingMessages;
41
+ private readyPending;
40
42
  constructor();
41
43
  /**
42
44
  * Connect to the WebSocket server
@@ -46,6 +48,7 @@ export declare class AgentWebSocket {
46
48
  * Set handler for incoming messages
47
49
  */
48
50
  onMessage(handler: MessageHandler): void;
51
+ sendReady(): void;
49
52
  /**
50
53
  * Send a streaming response for a message
51
54
  */
package/dist/websocket.js CHANGED
@@ -21,6 +21,8 @@ export class AgentWebSocket {
21
21
  isConnected = false;
22
22
  pendingResponses = new Map();
23
23
  cloudflareBlocked = false; // Track if Cloudflare is blocking WebSockets
24
+ pendingMessages = [];
25
+ readyPending = false;
24
26
  constructor() {
25
27
  const httpUrl = getServerUrl();
26
28
  // Check for WebSocket-specific URL (bypasses Cloudflare)
@@ -51,6 +53,12 @@ export class AgentWebSocket {
51
53
  this.isConnected = true;
52
54
  this.reconnectAttempts = 0;
53
55
  this.startPingInterval();
56
+ if (this.messageHandler) {
57
+ this.sendReady();
58
+ }
59
+ else {
60
+ this.readyPending = true;
61
+ }
54
62
  resolve(true);
55
63
  });
56
64
  this.ws.on('message', (data) => {
@@ -99,6 +107,20 @@ export class AgentWebSocket {
99
107
  */
100
108
  onMessage(handler) {
101
109
  this.messageHandler = handler;
110
+ if (this.isConnected && this.readyPending) {
111
+ this.readyPending = false;
112
+ this.sendReady();
113
+ }
114
+ if (this.pendingMessages.length > 0) {
115
+ const queued = [...this.pendingMessages];
116
+ this.pendingMessages = [];
117
+ for (const message of queued) {
118
+ void handler(message);
119
+ }
120
+ }
121
+ }
122
+ sendReady() {
123
+ this.sendToServer({ type: 'ready' });
102
124
  }
103
125
  /**
104
126
  * Send a streaming response for a message
@@ -251,6 +273,9 @@ export class AgentWebSocket {
251
273
  if (this.messageHandler && message.content) {
252
274
  this.messageHandler(message);
253
275
  }
276
+ else if (message.content) {
277
+ this.pendingMessages.push(message);
278
+ }
254
279
  break;
255
280
  case 'asset_request':
256
281
  // Handle asset requests (fast-path, no LLM)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siftd/connect-agent",
3
- "version": "0.2.46",
3
+ "version": "0.2.48",
4
4
  "description": "Master orchestrator agent - control Claude Code remotely via web",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",