@siftd/connect-agent 0.2.45 → 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;
@@ -751,8 +822,9 @@ export class MasterOrchestrator {
751
822
  const teamDir = this.getTeamFilesDir();
752
823
  if (!teamDir)
753
824
  return {};
825
+ const hasTeamDir = existsSync(teamDir);
754
826
  return {
755
- workingDir: this.forceTeamWorkingDir ? teamDir : undefined,
827
+ workingDir: this.forceTeamWorkingDir && hasTeamDir ? teamDir : undefined,
756
828
  instructions: `TEAM FILES DIRECTORY: ${teamDir}\nSave any requested files under this directory so they appear in /files (Team Files). Create the directory if needed.`,
757
829
  };
758
830
  }
@@ -774,12 +846,28 @@ export class MasterOrchestrator {
774
846
  * Process a user message
775
847
  */
776
848
  async processMessage(message, conversationHistory = [], sendMessage) {
849
+ this.lastUserMessage = message;
777
850
  // Handle slash commands first
778
851
  const slashResponse = this.handleSlashCommand(message);
779
852
  if (slashResponse) {
780
853
  return slashResponse;
781
854
  }
782
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
+ }
783
871
  // DISABLED: Dumb regex extraction was creating garbage todos
784
872
  // Let the AI use calendar_upsert_events and todo_upsert_items tools properly
785
873
  // const quickWrite = this.tryHandleCalendarTodo(message);
@@ -1230,14 +1318,34 @@ ${hubContextStr}
1230
1318
  * Run the agentic loop
1231
1319
  */
1232
1320
  async runAgentLoop(messages, system, sendMessage) {
1233
- 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);
1234
1340
  let currentMessages = [...messages];
1235
1341
  let iterations = 0;
1236
1342
  const maxIterations = 30; // Increased for complex multi-tool tasks
1237
1343
  const requestTimeoutMs = 60000;
1344
+ const forcedToolChoice = this.getToolChoice(currentMessages);
1345
+ let retriedForcedTool = false;
1238
1346
  while (iterations < maxIterations) {
1239
1347
  iterations++;
1240
- const toolChoice = this.getToolChoice(currentMessages);
1348
+ const toolChoice = forcedToolChoice ?? this.getToolChoice(currentMessages);
1241
1349
  const requestStart = Date.now();
1242
1350
  console.log(`[ORCHESTRATOR] Anthropic request ${iterations}/${maxIterations} (model: ${this.model})`);
1243
1351
  const response = await Promise.race([
@@ -1258,10 +1366,32 @@ ${hubContextStr}
1258
1366
  console.log(`[ORCHESTRATOR] Anthropic response ${iterations}/${maxIterations} in ${Date.now() - requestStart}ms`);
1259
1367
  // Check if done
1260
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
+ }
1261
1381
  return this.extractText(response.content);
1262
1382
  }
1383
+ const toolUseBlocks = response.content.filter((block) => block.type === 'tool_use');
1384
+ const toolNames = toolUseBlocks.map((block) => block.name);
1263
1385
  // Process tool calls
1264
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
+ }
1265
1395
  // Continue conversation
1266
1396
  currentMessages = [
1267
1397
  ...currentMessages,
@@ -1877,6 +2007,17 @@ Unlike lia_plan (internal only), this creates a VISIBLE todo list that appears i
1877
2007
  async processToolCalls(content, sendMessage) {
1878
2008
  const toolUseBlocks = content.filter((block) => block.type === 'tool_use');
1879
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
+ ]);
1880
2021
  for (const toolUse of toolUseBlocks) {
1881
2022
  const input = toolUse.input;
1882
2023
  let result;
@@ -1886,151 +2027,183 @@ Unlike lia_plan (internal only), this creates a VISIBLE todo list that appears i
1886
2027
  if (preview)
1887
2028
  await sendMessage(preview);
1888
2029
  }
1889
- switch (toolUse.name) {
1890
- // New direct tools
1891
- case 'bash':
1892
- result = await this.bashTool.execute(input.command, input.timeout);
1893
- break;
1894
- case 'calendar_upsert_events':
1895
- result = this.calendarTools.upsertCalendarEvents(input.events, input.calendars);
1896
- break;
1897
- case 'todo_upsert_items':
1898
- result = this.calendarTools.upsertTodoItems(input.items);
1899
- break;
1900
- case 'web_search':
1901
- result = await this.webTools.webSearch(input.query, { numResults: input.num_results });
1902
- break;
1903
- case 'fetch_url':
1904
- result = await this.webTools.fetchUrl(input.url, {
1905
- format: input.format,
1906
- timeout: input.timeout
1907
- });
1908
- break;
1909
- // Worker tools
1910
- case 'spawn_worker': {
1911
- const { task } = this.withAttachments(input.task);
1912
- const fileScope = this.getFileScopeOverrides();
1913
- const scopedTask = fileScope.instructions ? `${fileScope.instructions}\n\n${task}` : task;
1914
- result = await this.workerTools.spawnWorker(scopedTask, {
1915
- timeout: input.timeout,
1916
- priority: input.priority,
1917
- workingDirectory: input.working_directory || fileScope.workingDir
1918
- });
1919
- break;
1920
- }
1921
- case 'check_worker':
1922
- result = await this.workerTools.checkWorker(input.job_id);
1923
- break;
1924
- case 'wait_worker':
1925
- result = await this.workerTools.waitWorker(input.job_id, input.max_wait);
1926
- break;
1927
- case 'list_workers':
1928
- result = await this.workerTools.listWorkers(input.status);
1929
- break;
1930
- case 'cancel_worker':
1931
- result = await this.workerTools.cancelWorker(input.job_id);
1932
- break;
1933
- // Legacy delegate tool
1934
- case 'delegate_to_worker': {
1935
- const { task, context } = this.withAttachments(input.task, input.context);
1936
- const fileScope = this.getFileScopeOverrides();
1937
- result = await this.delegateToWorker(task, context, input.working_directory || fileScope.workingDir, fileScope.instructions);
1938
- break;
1939
- }
1940
- case 'remember':
1941
- result = await this.executeRemember(input.content, input.type, input.importance);
1942
- break;
1943
- case 'remember_entity': {
1944
- const scope = input.scope === 'org' ? 'org' : 'user';
1945
- const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
1946
- if (!graph) {
1947
- 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
+ });
1948
2092
  break;
1949
2093
  }
1950
- const entity = await graph.upsertEntity({
1951
- kind: input.kind,
1952
- name: input.name,
1953
- description: input.description,
1954
- attributes: input.attributes,
1955
- tags: input.tags
1956
- });
1957
- result = { success: true, output: `Stored ${scope} entity: ${entity.kind} ${entity.name} (${entity.key})` };
1958
- break;
1959
- }
1960
- case 'remember_relation': {
1961
- const scope = input.scope === 'org' ? 'org' : 'user';
1962
- const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
1963
- if (!graph) {
1964
- 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);
1965
2111
  break;
1966
2112
  }
1967
- const relation = await graph.linkEntities({
1968
- from: input.from,
1969
- to: input.to,
1970
- type: input.type,
1971
- description: input.description,
1972
- fromKind: input.from_kind,
1973
- toKind: input.to_kind
1974
- });
1975
- result = { success: true, output: `Linked ${scope}: ${relation.from} ${relation.type} ${relation.to}` };
1976
- break;
1977
- }
1978
- case 'search_context': {
1979
- const scope = input.scope === 'org' ? 'org' : 'user';
1980
- const graph = scope === 'org' ? this.orgContextGraph : this.contextGraph;
1981
- if (!graph) {
1982
- 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})` };
1983
2131
  break;
1984
2132
  }
1985
- const entities = await graph.searchEntities(input.query, { kind: input.kind, limit: 6 });
1986
- if (entities.length === 0) {
1987
- 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}` };
1988
2149
  break;
1989
2150
  }
1990
- const lines = entities.map(entity => {
1991
- const desc = entity.description ? ` - ${entity.description}` : '';
1992
- return `[${entity.kind}] ${entity.name}${desc} (${entity.key})`;
1993
- });
1994
- result = { success: true, output: lines.join('\n') };
1995
- 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}` };
1996
2206
  }
1997
- case 'search_memory':
1998
- result = await this.executeSearchMemory(input.query, input.type);
1999
- break;
2000
- case 'schedule_task':
2001
- result = await this.executeScheduleTask(input.task, input.when);
2002
- break;
2003
- case 'list_scheduled':
2004
- result = await this.executeListScheduled();
2005
- break;
2006
- case 'memory_stats':
2007
- result = await this.executeMemoryStats();
2008
- break;
2009
- case 'open_browser':
2010
- result = await this.executeOpenBrowser(input.url);
2011
- break;
2012
- case 'start_local_server':
2013
- result = await this.executeStartServer(input.directory, input.port);
2014
- break;
2015
- case 'stop_local_server':
2016
- result = await this.executeStopServer(input.port);
2017
- break;
2018
- // Lia's internal task management tools
2019
- case 'lia_plan':
2020
- result = this.executeLiaPlan(input.goal, input.steps);
2021
- break;
2022
- case 'lia_add_task':
2023
- result = this.executeLiaAddTask(input.task, input.priority);
2024
- break;
2025
- case 'lia_get_queue':
2026
- result = this.executeLiaGetQueue();
2027
- break;
2028
- case 'lia_todo_write':
2029
- result = this.executeLiaTodoWrite(input.todos);
2030
- break;
2031
- default:
2032
- result = { success: false, output: `Unknown tool: ${toolUse.name}` };
2033
- }
2034
2207
  // Anthropic API requires content to be non-empty when is_error is true
2035
2208
  // Belt-and-suspenders: ensure content is NEVER empty
2036
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.45",
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",