@siftd/connect-agent 0.2.52 → 0.2.53

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
  }
@@ -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.53",
4
4
  "description": "Master orchestrator agent - control Claude Code remotely via web",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",