@siftd/connect-agent 0.2.46 → 0.2.47

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,12 @@ export declare class MasterOrchestrator {
193
194
  private stripTodoSnapshot;
194
195
  private hasTodoMutation;
195
196
  private hasCalendarMutation;
196
- private hasExplicitDate;
197
+ private getUserTagHint;
198
+ private getBreakdownTarget;
199
+ private ensureBreakdownOriginalDone;
200
+ private getLocalDateKey;
201
+ private getLocalTimeZone;
202
+ private getTodoCalSystemPrompt;
197
203
  private getToolChoice;
198
204
  private withAttachments;
199
205
  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,66 @@ 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
+ getUserTagHint(message) {
705
+ if (!message)
706
+ return null;
707
+ const base = this.stripTodoSnapshot(message);
708
+ const matches = base.match(/\[[^\]]{2,64}\]/g) || [];
709
+ const unique = Array.from(new Set(matches.map((tag) => tag.trim()))).filter(Boolean);
710
+ if (unique.length !== 1)
711
+ return null;
712
+ return unique[0];
713
+ }
714
+ getBreakdownTarget(message) {
715
+ if (!message)
716
+ return null;
717
+ const stripped = this.stripTodoSnapshot(message);
718
+ if (!/\b(break(?:\s|-)?down|split|parse)\b/i.test(stripped))
719
+ return null;
720
+ const quoted = stripped.match(/["“]([^"”]{3,160})["”]/);
721
+ if (quoted && quoted[1]) {
722
+ return quoted[1].trim();
723
+ }
724
+ const inline = stripped.match(/break(?:\s|-)?down\s+the\s+(.+?)\s+(?:item|todo|task)\b/i);
725
+ if (inline && inline[1]) {
726
+ return inline[1].trim();
727
+ }
728
+ return null;
729
+ }
730
+ ensureBreakdownOriginalDone(items, targetTitle) {
731
+ const normalizedTarget = targetTitle.trim().toLowerCase();
732
+ let found = false;
733
+ const nextItems = items.map((item) => {
734
+ if (item?.title && item.title.trim().toLowerCase() === normalizedTarget) {
735
+ found = true;
736
+ return { ...item, done: true };
737
+ }
738
+ return item;
739
+ });
740
+ if (!found) {
741
+ nextItems.unshift({ title: targetTitle, done: true });
742
+ }
743
+ return nextItems;
744
+ }
745
+ getLocalDateKey(date = new Date()) {
746
+ const year = date.getFullYear();
747
+ const month = `${date.getMonth() + 1}`.padStart(2, '0');
748
+ const day = `${date.getDate()}`.padStart(2, '0');
749
+ return `${year}-${month}-${day}`;
750
+ }
751
+ getLocalTimeZone() {
752
+ try {
753
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || null;
754
+ }
755
+ catch {
756
+ return null;
757
+ }
758
+ }
759
+ getTodoCalSystemPrompt() {
760
+ const today = this.getLocalDateKey();
761
+ const timeZone = this.getLocalTimeZone();
762
+ const tzNote = timeZone ? ` (${timeZone})` : '';
763
+ 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
764
  }
694
765
  getToolChoice(messages) {
695
766
  const last = messages[messages.length - 1];
@@ -700,7 +771,7 @@ export class MasterOrchestrator {
700
771
  if (wantsTodo && !wantsCal) {
701
772
  return { type: 'tool', name: 'todo_upsert_items' };
702
773
  }
703
- if (wantsCal && !wantsTodo && this.hasExplicitDate(last.content)) {
774
+ if (wantsCal && !wantsTodo) {
704
775
  return { type: 'tool', name: 'calendar_upsert_events' };
705
776
  }
706
777
  return undefined;
@@ -775,12 +846,28 @@ export class MasterOrchestrator {
775
846
  * Process a user message
776
847
  */
777
848
  async processMessage(message, conversationHistory = [], sendMessage) {
849
+ this.lastUserMessage = message;
778
850
  // Handle slash commands first
779
851
  const slashResponse = this.handleSlashCommand(message);
780
852
  if (slashResponse) {
781
853
  return slashResponse;
782
854
  }
783
855
  this.updateFileScope(message);
856
+ const wantsTodoOrCal = this.hasTodoMutation(message) || this.hasCalendarMutation(message);
857
+ if (wantsTodoOrCal) {
858
+ this.attachmentContext = this.extractAttachmentContext(message);
859
+ const toolMessages = [{ role: 'user', content: message }];
860
+ try {
861
+ return await this.runAgentLoop(toolMessages, this.getTodoCalSystemPrompt(), sendMessage);
862
+ }
863
+ catch (error) {
864
+ const errorMessage = error instanceof Error ? error.message : String(error);
865
+ return `Error: ${errorMessage}`;
866
+ }
867
+ finally {
868
+ this.attachmentContext = null;
869
+ }
870
+ }
784
871
  // DISABLED: Dumb regex extraction was creating garbage todos
785
872
  // Let the AI use calendar_upsert_events and todo_upsert_items tools properly
786
873
  // const quickWrite = this.tryHandleCalendarTodo(message);
@@ -1231,14 +1318,34 @@ ${hubContextStr}
1231
1318
  * Run the agentic loop
1232
1319
  */
1233
1320
  async runAgentLoop(messages, system, sendMessage) {
1234
- const tools = this.getToolDefinitions();
1321
+ const toolsBase = this.getToolDefinitions();
1322
+ const last = messages[messages.length - 1];
1323
+ const lastContent = last && last.role === 'user' && typeof last.content === 'string' ? last.content : '';
1324
+ const restrictWorkers = lastContent
1325
+ ? (this.hasTodoMutation(lastContent) || this.hasCalendarMutation(lastContent))
1326
+ : false;
1327
+ const wantsTodoOrCal = restrictWorkers;
1328
+ const todoCalTools = new Set(['calendar_upsert_events', 'todo_upsert_items']);
1329
+ const blockedTools = new Set([
1330
+ 'spawn_worker',
1331
+ 'delegate_to_worker',
1332
+ 'check_worker',
1333
+ 'wait_worker',
1334
+ 'list_workers',
1335
+ 'cancel_worker',
1336
+ ]);
1337
+ const tools = wantsTodoOrCal
1338
+ ? toolsBase.filter((tool) => todoCalTools.has(tool.name))
1339
+ : toolsBase.filter((tool) => !blockedTools.has(tool.name) || !restrictWorkers);
1235
1340
  let currentMessages = [...messages];
1236
1341
  let iterations = 0;
1237
1342
  const maxIterations = 30; // Increased for complex multi-tool tasks
1238
1343
  const requestTimeoutMs = 60000;
1344
+ const forcedToolChoice = this.getToolChoice(currentMessages);
1345
+ let retriedForcedTool = false;
1239
1346
  while (iterations < maxIterations) {
1240
1347
  iterations++;
1241
- const toolChoice = this.getToolChoice(currentMessages);
1348
+ const toolChoice = forcedToolChoice ?? this.getToolChoice(currentMessages);
1242
1349
  const requestStart = Date.now();
1243
1350
  console.log(`[ORCHESTRATOR] Anthropic request ${iterations}/${maxIterations} (model: ${this.model})`);
1244
1351
  const response = await Promise.race([
@@ -1259,10 +1366,32 @@ ${hubContextStr}
1259
1366
  console.log(`[ORCHESTRATOR] Anthropic response ${iterations}/${maxIterations} in ${Date.now() - requestStart}ms`);
1260
1367
  // Check if done
1261
1368
  if (response.stop_reason === 'end_turn' || !this.hasToolUse(response.content)) {
1369
+ if (forcedToolChoice && !retriedForcedTool) {
1370
+ retriedForcedTool = true;
1371
+ currentMessages = [
1372
+ ...currentMessages,
1373
+ { role: 'assistant', content: response.content },
1374
+ {
1375
+ role: 'user',
1376
+ content: `You must call the ${forcedToolChoice.name} tool now. Use the exact task/event titles and any bracketed tags exactly as provided. Do not spawn workers.`
1377
+ }
1378
+ ];
1379
+ continue;
1380
+ }
1262
1381
  return this.extractText(response.content);
1263
1382
  }
1383
+ const toolUseBlocks = response.content.filter((block) => block.type === 'tool_use');
1384
+ const toolNames = toolUseBlocks.map((block) => block.name);
1264
1385
  // Process tool calls
1265
1386
  const toolResults = await this.processToolCalls(response.content, sendMessage);
1387
+ if (wantsTodoOrCal && (toolNames.includes('todo_upsert_items') || toolNames.includes('calendar_upsert_events'))) {
1388
+ const updates = [];
1389
+ if (toolNames.includes('todo_upsert_items'))
1390
+ updates.push('Updated /todo.');
1391
+ if (toolNames.includes('calendar_upsert_events'))
1392
+ updates.push('Updated /cal.');
1393
+ return `${updates.join(' ')} Open /todo or /cal to view.`;
1394
+ }
1266
1395
  // Continue conversation
1267
1396
  currentMessages = [
1268
1397
  ...currentMessages,
@@ -1878,6 +2007,17 @@ Unlike lia_plan (internal only), this creates a VISIBLE todo list that appears i
1878
2007
  async processToolCalls(content, sendMessage) {
1879
2008
  const toolUseBlocks = content.filter((block) => block.type === 'tool_use');
1880
2009
  const results = [];
2010
+ const wantsTodoOrCal = this.lastUserMessage
2011
+ ? (this.hasTodoMutation(this.lastUserMessage) || this.hasCalendarMutation(this.lastUserMessage))
2012
+ : false;
2013
+ const blockedWorkerTools = new Set([
2014
+ 'spawn_worker',
2015
+ 'delegate_to_worker',
2016
+ 'check_worker',
2017
+ 'wait_worker',
2018
+ 'list_workers',
2019
+ 'cancel_worker',
2020
+ ]);
1881
2021
  for (const toolUse of toolUseBlocks) {
1882
2022
  const input = toolUse.input;
1883
2023
  let result;
@@ -1887,151 +2027,183 @@ Unlike lia_plan (internal only), this creates a VISIBLE todo list that appears i
1887
2027
  if (preview)
1888
2028
  await sendMessage(preview);
1889
2029
  }
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.' };
2030
+ if (wantsTodoOrCal && blockedWorkerTools.has(toolUse.name)) {
2031
+ result = {
2032
+ success: false,
2033
+ output: '',
2034
+ error: 'Do not use worker tools for /todo or /cal. Call todo_upsert_items or calendar_upsert_events directly.'
2035
+ };
2036
+ }
2037
+ else
2038
+ switch (toolUse.name) {
2039
+ // New direct tools
2040
+ case 'bash':
2041
+ result = await this.bashTool.execute(input.command, input.timeout);
2042
+ break;
2043
+ case 'calendar_upsert_events':
2044
+ {
2045
+ const tagHint = this.getUserTagHint(this.lastUserMessage);
2046
+ const events = input.events || [];
2047
+ const taggedEvents = tagHint
2048
+ ? events.map((event) => ({
2049
+ ...event,
2050
+ title: event.title.includes(tagHint) ? event.title : `${event.title} ${tagHint}`
2051
+ }))
2052
+ : events;
2053
+ result = this.calendarTools.upsertCalendarEvents(taggedEvents, input.calendars);
2054
+ }
2055
+ break;
2056
+ case 'todo_upsert_items':
2057
+ {
2058
+ const tagHint = this.getUserTagHint(this.lastUserMessage);
2059
+ let items = input.items || [];
2060
+ const breakdownTarget = this.getBreakdownTarget(this.lastUserMessage);
2061
+ if (breakdownTarget) {
2062
+ items = this.ensureBreakdownOriginalDone(items, breakdownTarget);
2063
+ }
2064
+ const taggedItems = tagHint
2065
+ ? items.map((item) => ({
2066
+ ...item,
2067
+ title: item.title.includes(tagHint) ? item.title : `${item.title} ${tagHint}`
2068
+ }))
2069
+ : items;
2070
+ result = this.calendarTools.upsertTodoItems(taggedItems);
2071
+ }
2072
+ break;
2073
+ case 'web_search':
2074
+ result = await this.webTools.webSearch(input.query, { numResults: input.num_results });
2075
+ break;
2076
+ case 'fetch_url':
2077
+ result = await this.webTools.fetchUrl(input.url, {
2078
+ format: input.format,
2079
+ timeout: input.timeout
2080
+ });
2081
+ break;
2082
+ // Worker tools
2083
+ case 'spawn_worker': {
2084
+ const { task } = this.withAttachments(input.task);
2085
+ const fileScope = this.getFileScopeOverrides();
2086
+ const scopedTask = fileScope.instructions ? `${fileScope.instructions}\n\n${task}` : task;
2087
+ result = await this.workerTools.spawnWorker(scopedTask, {
2088
+ timeout: input.timeout,
2089
+ priority: input.priority,
2090
+ workingDirectory: input.working_directory || fileScope.workingDir
2091
+ });
1949
2092
  break;
1950
2093
  }
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.' };
2094
+ case 'check_worker':
2095
+ result = await this.workerTools.checkWorker(input.job_id);
2096
+ break;
2097
+ case 'wait_worker':
2098
+ result = await this.workerTools.waitWorker(input.job_id, input.max_wait);
2099
+ break;
2100
+ case 'list_workers':
2101
+ result = await this.workerTools.listWorkers(input.status);
2102
+ break;
2103
+ case 'cancel_worker':
2104
+ result = await this.workerTools.cancelWorker(input.job_id);
2105
+ break;
2106
+ // Legacy delegate tool
2107
+ case 'delegate_to_worker': {
2108
+ const { task, context } = this.withAttachments(input.task, input.context);
2109
+ const fileScope = this.getFileScopeOverrides();
2110
+ result = await this.delegateToWorker(task, context, input.working_directory || fileScope.workingDir, fileScope.instructions);
1966
2111
  break;
1967
2112
  }
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.' };
2113
+ case 'remember':
2114
+ result = await this.executeRemember(input.content, input.type, input.importance);
2115
+ break;
2116
+ case 'remember_entity': {
2117
+ const scope = input.scope === 'org' ? 'org' : 'user';
2118
+ const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
2119
+ if (!graph) {
2120
+ result = { success: false, output: 'Org context is not configured for this agent.' };
2121
+ break;
2122
+ }
2123
+ const entity = await graph.upsertEntity({
2124
+ kind: input.kind,
2125
+ name: input.name,
2126
+ description: input.description,
2127
+ attributes: input.attributes,
2128
+ tags: input.tags
2129
+ });
2130
+ result = { success: true, output: `Stored ${scope} entity: ${entity.kind} ${entity.name} (${entity.key})` };
1984
2131
  break;
1985
2132
  }
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.' };
2133
+ case 'remember_relation': {
2134
+ const scope = input.scope === 'org' ? 'org' : 'user';
2135
+ const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
2136
+ if (!graph) {
2137
+ result = { success: false, output: 'Org context is not configured for this agent.' };
2138
+ break;
2139
+ }
2140
+ const relation = await graph.linkEntities({
2141
+ from: input.from,
2142
+ to: input.to,
2143
+ type: input.type,
2144
+ description: input.description,
2145
+ fromKind: input.from_kind,
2146
+ toKind: input.to_kind
2147
+ });
2148
+ result = { success: true, output: `Linked ${scope}: ${relation.from} ${relation.type} ${relation.to}` };
1989
2149
  break;
1990
2150
  }
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;
2151
+ case 'search_context': {
2152
+ const scope = input.scope === 'org' ? 'org' : 'user';
2153
+ const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
2154
+ if (!graph) {
2155
+ result = { success: false, output: 'Org context is not configured for this agent.' };
2156
+ break;
2157
+ }
2158
+ const entities = await graph.searchEntities(input.query, { kind: input.kind, limit: 6 });
2159
+ if (entities.length === 0) {
2160
+ result = { success: true, output: 'No context entities found.' };
2161
+ break;
2162
+ }
2163
+ const lines = entities.map(entity => {
2164
+ const desc = entity.description ? ` - ${entity.description}` : '';
2165
+ return `[${entity.kind}] ${entity.name}${desc} (${entity.key})`;
2166
+ });
2167
+ result = { success: true, output: lines.join('\n') };
2168
+ break;
2169
+ }
2170
+ case 'search_memory':
2171
+ result = await this.executeSearchMemory(input.query, input.type);
2172
+ break;
2173
+ case 'schedule_task':
2174
+ result = await this.executeScheduleTask(input.task, input.when);
2175
+ break;
2176
+ case 'list_scheduled':
2177
+ result = await this.executeListScheduled();
2178
+ break;
2179
+ case 'memory_stats':
2180
+ result = await this.executeMemoryStats();
2181
+ break;
2182
+ case 'open_browser':
2183
+ result = await this.executeOpenBrowser(input.url);
2184
+ break;
2185
+ case 'start_local_server':
2186
+ result = await this.executeStartServer(input.directory, input.port);
2187
+ break;
2188
+ case 'stop_local_server':
2189
+ result = await this.executeStopServer(input.port);
2190
+ break;
2191
+ // Lia's internal task management tools
2192
+ case 'lia_plan':
2193
+ result = this.executeLiaPlan(input.goal, input.steps);
2194
+ break;
2195
+ case 'lia_add_task':
2196
+ result = this.executeLiaAddTask(input.task, input.priority);
2197
+ break;
2198
+ case 'lia_get_queue':
2199
+ result = this.executeLiaGetQueue();
2200
+ break;
2201
+ case 'lia_todo_write':
2202
+ result = this.executeLiaTodoWrite(input.todos);
2203
+ break;
2204
+ default:
2205
+ result = { success: false, output: `Unknown tool: ${toolUse.name}` };
1997
2206
  }
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
2207
  // Anthropic API requires content to be non-empty when is_error is true
2036
2208
  // Belt-and-suspenders: ensure content is NEVER empty
2037
2209
  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.47",
4
4
  "description": "Master orchestrator agent - control Claude Code remotely via web",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",