@siftd/connect-agent 0.2.52 → 0.2.54

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.
package/README.md CHANGED
@@ -133,6 +133,22 @@ export function buildWorkerPrompt(task, options) { ... }
133
133
  - `VOYAGE_API_KEY` - (Optional) For better semantic search embeddings
134
134
  - `DATABASE_URL` - (Optional) PostgreSQL for cloud memory persistence
135
135
 
136
+ ### Model + Cost Controls (Optional)
137
+
138
+ - `LIA_FORCE_OPUS` - Defaults to `1`; set to `0` to allow model overrides
139
+ - `LIA_MODEL` - Explicit model override (defaults to Opus 4.5 when forced)
140
+ - `LIA_MAX_BUDGET_USD` - Per-worker budget cap (USD) for Claude Code CLI
141
+ - `LIA_TOOL_MAX_TOKENS` - Max tokens for `/todo` + `/cal` tool calls
142
+ - `LIA_WORKER_VERBOSE` - Set to `1` for verbose worker logging
143
+
144
+ ### Usage Logging
145
+
146
+ Anthropic usage is logged per channel in:
147
+
148
+ ```
149
+ ~/Lia-Hub/shared/outputs/.lia/usage.jsonl
150
+ ```
151
+
136
152
  ## Requirements
137
153
 
138
154
  - Node.js 18+
package/dist/agent.js CHANGED
@@ -423,46 +423,63 @@ export async function runAgent(pollInterval = 2000) {
423
423
  });
424
424
  if (wsConnected) {
425
425
  console.log('[AGENT] Using WebSocket for real-time communication\n');
426
- // Handle messages via WebSocket
427
- wsClient.onMessage(async (wsMsg) => {
428
- if (wsMsg.type === 'message' && wsMsg.content && wsMsg.id) {
429
- const message = {
430
- id: wsMsg.id,
431
- content: wsMsg.content,
432
- timestamp: wsMsg.timestamp || Date.now()
433
- };
434
- const response = await processMessage(message);
426
+ }
427
+ else {
428
+ console.log('[AGENT] WebSocket unavailable, using HTTP polling\n');
429
+ }
430
+ // Handle messages via WebSocket
431
+ wsClient.onMessage(async (wsMsg) => {
432
+ if (wsMsg.type === 'message' && wsMsg.content && wsMsg.id) {
433
+ const message = {
434
+ id: wsMsg.id,
435
+ content: wsMsg.content,
436
+ timestamp: wsMsg.timestamp || Date.now()
437
+ };
438
+ const response = await processMessage(message);
439
+ if (wsClient?.connected()) {
435
440
  // Send response via WebSocket
436
441
  wsClient.sendResponse(wsMsg.id, response);
437
442
  console.log(`[AGENT] Response sent via WebSocket (${response.length} chars)`);
438
443
  }
439
- });
440
- // Keep process alive - WebSocket handles messages
441
- console.log('[AGENT] Listening for WebSocket messages...\n');
442
- // Poll only if WebSocket is not connected (WS input is preferred for latency).
443
- while (true) {
444
- await new Promise(resolve => setTimeout(resolve, pollInterval));
445
- if (!wsClient?.connected()) {
446
- try {
447
- const { messages } = await pollMessages();
448
- for (const msg of messages) {
449
- const response = await processMessage(msg);
450
- await sendResponse(msg.id, response);
451
- console.log(`[AGENT] Response sent (${response.length} chars)`);
452
- }
453
- }
454
- catch (error) {
455
- console.error('[AGENT] Poll error:', error.message);
456
- }
444
+ else {
445
+ await sendResponse(wsMsg.id, response);
446
+ console.log(`[AGENT] Response sent via HTTP (${response.length} chars)`);
457
447
  }
458
448
  }
459
- }
460
- else {
461
- // Fall back to polling
462
- console.log('[AGENT] WebSocket unavailable, using HTTP polling\n');
463
- while (true) {
449
+ });
450
+ console.log('[AGENT] Listening for WebSocket messages...\n');
451
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
452
+ const basePollInterval = pollInterval;
453
+ const maxPollBackoff = 30000;
454
+ const wsReconnectInterval = 30000;
455
+ let pollErrorCount = 0;
456
+ let lastPollErrorLog = 0;
457
+ let lastWsReconnectAttempt = Date.now();
458
+ let nextPollDelay = basePollInterval;
459
+ const logPollError = (message) => {
460
+ const now = Date.now();
461
+ if (now - lastPollErrorLog > 10000) {
462
+ lastPollErrorLog = now;
463
+ console.error('[AGENT] Poll error:', message);
464
+ }
465
+ };
466
+ const computeBackoff = (count) => {
467
+ const exponent = Math.min(count, 5);
468
+ const delay = Math.min(maxPollBackoff, basePollInterval * Math.pow(2, exponent));
469
+ const jitter = delay * 0.1 * (Math.random() * 2 - 1);
470
+ return Math.max(basePollInterval, Math.round(delay + jitter));
471
+ };
472
+ while (true) {
473
+ const now = Date.now();
474
+ if (!wsClient?.connected() && now - lastWsReconnectAttempt > wsReconnectInterval) {
475
+ lastWsReconnectAttempt = now;
476
+ void wsClient.connect();
477
+ }
478
+ if (!wsClient?.connected()) {
464
479
  try {
465
480
  const { messages } = await pollMessages();
481
+ pollErrorCount = 0;
482
+ nextPollDelay = basePollInterval;
466
483
  for (const msg of messages) {
467
484
  const response = await processMessage(msg);
468
485
  await sendResponse(msg.id, response);
@@ -470,11 +487,22 @@ export async function runAgent(pollInterval = 2000) {
470
487
  }
471
488
  }
472
489
  catch (error) {
473
- if (error instanceof Error && !error.message.includes('ECONNREFUSED')) {
474
- console.error('[AGENT] Poll error:', error.message);
490
+ pollErrorCount += 1;
491
+ const status = error.status;
492
+ const message = error instanceof Error ? error.message : String(error);
493
+ logPollError(message);
494
+ if (status === 401 || status === 403) {
495
+ nextPollDelay = maxPollBackoff;
496
+ }
497
+ else {
498
+ nextPollDelay = computeBackoff(pollErrorCount);
475
499
  }
476
500
  }
477
- await new Promise(resolve => setTimeout(resolve, pollInterval));
478
501
  }
502
+ else {
503
+ pollErrorCount = 0;
504
+ nextPollDelay = basePollInterval;
505
+ }
506
+ await sleep(nextPollDelay);
479
507
  }
480
508
  }
package/dist/api.js CHANGED
@@ -26,7 +26,9 @@ export async function connectWithPairingCode(code) {
26
26
  export async function pollMessages() {
27
27
  const res = await fetchWithAuth('/api/agent/messages');
28
28
  if (!res.ok) {
29
- throw new Error(`Failed to poll messages: ${res.status}`);
29
+ const error = new Error(`Failed to poll messages: ${res.status}`);
30
+ error.status = res.status;
31
+ throw error;
30
32
  }
31
33
  return res.json();
32
34
  }
@@ -734,8 +734,8 @@ export class MasterOrchestrator {
734
734
  }
735
735
  hasCalendarMutation(message) {
736
736
  const lower = this.stripTodoSnapshot(message).toLowerCase();
737
- const target = /(^|\s)\/cal\b|\/calendar\b|\bcalendar\b|\bcal\b/.test(lower);
738
- const action = /\b(add|create|schedule|book|move|reschedule|update|change|cancel|delete|remove)\b/.test(lower);
737
+ const target = /(^|\s)\/cal\b|\/calendar\b|\bcalendar\b|\bcal\b|\bschedule\b|\bagenda\b/.test(lower);
738
+ const action = /\b(add|create|schedule|book|move|reschedule|update|change|cancel|delete|remove|set|put|place|remind)\b/.test(lower);
739
739
  const query = /\b(what|show|list|open|view|see)\b/.test(lower);
740
740
  return target && action && !query;
741
741
  }
@@ -1422,9 +1422,9 @@ ${hubContextStr}
1422
1422
  const toolsBase = this.getToolDefinitions();
1423
1423
  const last = messages[messages.length - 1];
1424
1424
  const lastContent = last && last.role === 'user' && typeof last.content === 'string' ? last.content : '';
1425
- const restrictWorkers = lastContent
1426
- ? (this.hasTodoMutation(lastContent) || this.hasCalendarMutation(lastContent))
1427
- : false;
1425
+ const requiredTodo = lastContent ? this.hasTodoMutation(lastContent) : false;
1426
+ const requiredCal = lastContent ? this.hasCalendarMutation(lastContent) : false;
1427
+ const restrictWorkers = requiredTodo || requiredCal;
1428
1428
  const wantsTodoOrCal = restrictWorkers;
1429
1429
  const todoCalTools = new Set(['calendar_upsert_events', 'todo_upsert_items']);
1430
1430
  const blockedTools = new Set([
@@ -1444,6 +1444,7 @@ ${hubContextStr}
1444
1444
  const requestTimeoutMs = 60000;
1445
1445
  const forcedToolChoice = this.getToolChoice(currentMessages);
1446
1446
  let retriedForcedTool = false;
1447
+ let retriedTodoCal = false;
1447
1448
  while (iterations < maxIterations) {
1448
1449
  iterations++;
1449
1450
  const toolChoice = forcedToolChoice ?? this.getToolChoice(currentMessages);
@@ -1492,13 +1493,44 @@ ${hubContextStr}
1492
1493
  ];
1493
1494
  continue;
1494
1495
  }
1496
+ if (wantsTodoOrCal && !retriedTodoCal && (requiredTodo || requiredCal)) {
1497
+ retriedTodoCal = true;
1498
+ const required = [
1499
+ requiredTodo ? 'todo_upsert_items' : null,
1500
+ requiredCal ? 'calendar_upsert_events' : null
1501
+ ].filter(Boolean).join(' and ');
1502
+ const followup = `You must call ${required} now. Do not spawn workers or call any other tools.`;
1503
+ currentMessages = [
1504
+ ...currentMessages,
1505
+ { role: 'assistant', content: response.content },
1506
+ { role: 'user', content: followup }
1507
+ ];
1508
+ continue;
1509
+ }
1495
1510
  return this.extractText(response.content);
1496
1511
  }
1497
1512
  const toolUseBlocks = response.content.filter((block) => block.type === 'tool_use');
1498
1513
  const toolNames = toolUseBlocks.map((block) => block.name);
1499
1514
  // Process tool calls
1500
1515
  const toolResults = await this.processToolCalls(response.content, sendMessage);
1501
- if (wantsTodoOrCal && (toolNames.includes('todo_upsert_items') || toolNames.includes('calendar_upsert_events'))) {
1516
+ const missingTodo = requiredTodo && !toolNames.includes('todo_upsert_items');
1517
+ const missingCal = requiredCal && !toolNames.includes('calendar_upsert_events');
1518
+ if (wantsTodoOrCal && (missingTodo || missingCal) && !retriedTodoCal) {
1519
+ retriedTodoCal = true;
1520
+ const required = [
1521
+ missingTodo ? 'todo_upsert_items' : null,
1522
+ missingCal ? 'calendar_upsert_events' : null
1523
+ ].filter(Boolean).join(' and ');
1524
+ const followup = `You must call ${required} now. Do not repeat tools already called. Do not spawn workers.`;
1525
+ currentMessages = [
1526
+ ...currentMessages,
1527
+ { role: 'assistant', content: response.content },
1528
+ { role: 'user', content: toolResults },
1529
+ { role: 'user', content: followup }
1530
+ ];
1531
+ continue;
1532
+ }
1533
+ if (wantsTodoOrCal && !missingTodo && !missingCal) {
1502
1534
  const updates = [];
1503
1535
  if (toolNames.includes('todo_upsert_items'))
1504
1536
  updates.push('Updated /todo.');
@@ -35,6 +35,8 @@ export declare class AgentWebSocket {
35
35
  private reconnectDelay;
36
36
  private pingInterval;
37
37
  private isConnected;
38
+ private connecting;
39
+ private connectPromise;
38
40
  private pendingResponses;
39
41
  private cloudflareBlocked;
40
42
  private pendingMessages;
package/dist/websocket.js CHANGED
@@ -19,6 +19,8 @@ export class AgentWebSocket {
19
19
  reconnectDelay = 1000;
20
20
  pingInterval = null;
21
21
  isConnected = false;
22
+ connecting = false;
23
+ connectPromise = null;
22
24
  pendingResponses = new Map();
23
25
  cloudflareBlocked = false; // Track if Cloudflare is blocking WebSockets
24
26
  pendingMessages = [];
@@ -40,7 +42,32 @@ export class AgentWebSocket {
40
42
  * Connect to the WebSocket server
41
43
  */
42
44
  async connect() {
43
- return new Promise((resolve) => {
45
+ if (this.connected()) {
46
+ return Promise.resolve(true);
47
+ }
48
+ if (this.connecting && this.connectPromise) {
49
+ return this.connectPromise;
50
+ }
51
+ if (this.ws && this.ws.readyState !== WebSocket.OPEN) {
52
+ try {
53
+ this.ws.terminate();
54
+ }
55
+ catch {
56
+ // Ignore terminate errors
57
+ }
58
+ this.ws = null;
59
+ }
60
+ this.connecting = true;
61
+ this.connectPromise = new Promise((resolve) => {
62
+ let settled = false;
63
+ const finalize = (value) => {
64
+ if (settled)
65
+ return;
66
+ settled = true;
67
+ this.connecting = false;
68
+ this.connectPromise = null;
69
+ resolve(value);
70
+ };
44
71
  try {
45
72
  console.log('[WS] Connecting to', this.serverUrl);
46
73
  this.ws = new WebSocket(this.serverUrl, {
@@ -59,7 +86,7 @@ export class AgentWebSocket {
59
86
  else {
60
87
  this.readyPending = true;
61
88
  }
62
- resolve(true);
89
+ finalize(true);
63
90
  });
64
91
  this.ws.on('message', (data) => {
65
92
  this.handleMessage(data.toString());
@@ -72,6 +99,7 @@ export class AgentWebSocket {
72
99
  this.isConnected = false;
73
100
  this.stopPingInterval();
74
101
  this.attemptReconnect();
102
+ finalize(false);
75
103
  });
76
104
  this.ws.on('error', (error) => {
77
105
  // Check for Cloudflare 524 timeout - don't spam logs
@@ -85,22 +113,23 @@ export class AgentWebSocket {
85
113
  console.error('[WS] Error:', error.message);
86
114
  }
87
115
  if (!this.isConnected) {
88
- resolve(false);
116
+ finalize(false);
89
117
  }
90
118
  });
91
119
  // Timeout for initial connection
92
120
  setTimeout(() => {
93
121
  if (!this.isConnected) {
94
122
  console.log('[WS] Connection timeout');
95
- resolve(false);
123
+ finalize(false);
96
124
  }
97
125
  }, 10000);
98
126
  }
99
127
  catch (error) {
100
128
  console.error('[WS] Connection failed:', error);
101
- resolve(false);
129
+ finalize(false);
102
130
  }
103
131
  });
132
+ return this.connectPromise;
104
133
  }
105
134
  /**
106
135
  * Set handler for incoming messages
@@ -339,7 +368,9 @@ export class AgentWebSocket {
339
368
  this.reconnectAttempts++;
340
369
  const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
341
370
  setTimeout(() => {
342
- this.connect();
371
+ if (!this.connecting) {
372
+ this.connect();
373
+ }
343
374
  }, delay);
344
375
  }
345
376
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siftd/connect-agent",
3
- "version": "0.2.52",
3
+ "version": "0.2.54",
4
4
  "description": "Master orchestrator agent - control Claude Code remotely via web",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",