@siftd/connect-agent 0.2.51 → 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
  }
@@ -49,6 +49,7 @@ export declare class MasterOrchestrator {
49
49
  private client;
50
50
  private model;
51
51
  private maxTokens;
52
+ private toolMaxTokens;
52
53
  private memory;
53
54
  private contextGraph;
54
55
  private orgMemory?;
@@ -202,6 +203,7 @@ export declare class MasterOrchestrator {
202
203
  private getLocalTimeZone;
203
204
  private getTodoCalSystemPrompt;
204
205
  private getToolChoice;
206
+ private recordUsage;
205
207
  private withAttachments;
206
208
  private updateFileScope;
207
209
  private getTeamFilesDir;
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import Anthropic from '@anthropic-ai/sdk';
8
8
  import { spawn, execSync } from 'child_process';
9
- import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
9
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync } from 'fs';
10
10
  import { join, resolve, dirname, sep } from 'path';
11
11
  import { AdvancedMemoryStore } from './core/memory-advanced.js';
12
12
  import { PostgresMemoryStore, isPostgresConfigured } from './core/memory-postgres.js';
@@ -22,6 +22,35 @@ import { getKnowledgeForPrompt } from './genesis/index.js';
22
22
  import { loadHubContext, formatHubContext, logAction, logWorker, getSharedOutputPath } from './core/hub.js';
23
23
  import { buildWorkerPrompt } from './prompts/worker-system.js';
24
24
  import { LiaTaskQueue } from './core/task-queue.js';
25
+ const DEFAULT_CLAUDE_MODEL = 'claude-opus-4-5-20251101';
26
+ const DEFAULT_TOOL_MAX_TOKENS = 1024;
27
+ function readBooleanEnv(value, fallback) {
28
+ if (value === undefined)
29
+ return fallback;
30
+ return !['0', 'false', 'no', 'off'].includes(value.trim().toLowerCase());
31
+ }
32
+ function resolveClaudeModel() {
33
+ const raw = (process.env.LIA_MODEL || process.env.ANTHROPIC_MODEL || DEFAULT_CLAUDE_MODEL).trim();
34
+ if (!raw)
35
+ return DEFAULT_CLAUDE_MODEL;
36
+ if (readBooleanEnv(process.env.LIA_FORCE_OPUS, true) && raw !== DEFAULT_CLAUDE_MODEL) {
37
+ return DEFAULT_CLAUDE_MODEL;
38
+ }
39
+ return raw;
40
+ }
41
+ function resolveClaudeBudgetUsd() {
42
+ const raw = process.env.LIA_MAX_BUDGET_USD || process.env.CLAUDE_MAX_BUDGET_USD;
43
+ if (!raw)
44
+ return null;
45
+ const trimmed = raw.trim();
46
+ return trimmed ? trimmed : null;
47
+ }
48
+ function resolveToolMaxTokens() {
49
+ const raw = Number(process.env.LIA_TOOL_MAX_TOKENS);
50
+ if (Number.isFinite(raw) && raw >= 256)
51
+ return Math.min(raw, 4096);
52
+ return DEFAULT_TOOL_MAX_TOKENS;
53
+ }
25
54
  /**
26
55
  * Extract file paths from worker output
27
56
  * Workers naturally mention files they create: "Created /tmp/foo.html", "Saved to /path/file"
@@ -203,6 +232,7 @@ export class MasterOrchestrator {
203
232
  client;
204
233
  model;
205
234
  maxTokens;
235
+ toolMaxTokens;
206
236
  memory;
207
237
  contextGraph;
208
238
  orgMemory;
@@ -244,8 +274,14 @@ export class MasterOrchestrator {
244
274
  todoUpdateCallback;
245
275
  constructor(options) {
246
276
  this.client = new Anthropic({ apiKey: options.apiKey });
247
- this.model = options.model || process.env.ANTHROPIC_MODEL || 'claude-opus-4-5-20251101';
277
+ const requestedModel = options.model?.trim();
278
+ this.model = requestedModel
279
+ ? (readBooleanEnv(process.env.LIA_FORCE_OPUS, true) && requestedModel !== DEFAULT_CLAUDE_MODEL
280
+ ? DEFAULT_CLAUDE_MODEL
281
+ : requestedModel)
282
+ : resolveClaudeModel();
248
283
  this.maxTokens = options.maxTokens || 4096;
284
+ this.toolMaxTokens = resolveToolMaxTokens();
249
285
  this.userId = options.userId;
250
286
  this.orgId = options.orgId;
251
287
  this.orgRole = options.orgRole;
@@ -698,7 +734,7 @@ export class MasterOrchestrator {
698
734
  }
699
735
  hasCalendarMutation(message) {
700
736
  const lower = this.stripTodoSnapshot(message).toLowerCase();
701
- const target = /(^|\s)\/cal\b|\/calendar\b|\bcalendar\b/.test(lower);
737
+ const target = /(^|\s)\/cal\b|\/calendar\b|\bcalendar\b|\bcal\b/.test(lower);
702
738
  const action = /\b(add|create|schedule|book|move|reschedule|update|change|cancel|delete|remove)\b/.test(lower);
703
739
  const query = /\b(what|show|list|open|view|see)\b/.test(lower);
704
740
  return target && action && !query;
@@ -789,6 +825,32 @@ export class MasterOrchestrator {
789
825
  }
790
826
  return undefined;
791
827
  }
828
+ recordUsage(response, meta) {
829
+ const usage = response.usage;
830
+ if (!usage)
831
+ return;
832
+ try {
833
+ const usageDir = join(getSharedOutputPath(), '.lia');
834
+ mkdirSync(usageDir, { recursive: true });
835
+ const entry = {
836
+ ts: new Date().toISOString(),
837
+ model: this.model,
838
+ input_tokens: usage.input_tokens,
839
+ output_tokens: usage.output_tokens,
840
+ cache_creation_input_tokens: usage.cache_creation_input_tokens,
841
+ cache_read_input_tokens: usage.cache_read_input_tokens,
842
+ tool_choice: meta.toolChoice || null,
843
+ iterations: meta.iterations,
844
+ messages: meta.messages,
845
+ stop_reason: response.stop_reason || null,
846
+ response_id: response.id || null,
847
+ };
848
+ appendFileSync(join(usageDir, 'usage.jsonl'), `${JSON.stringify(entry)}\n`, 'utf8');
849
+ }
850
+ catch (error) {
851
+ console.warn('[ORCHESTRATOR] Usage log failed:', error);
852
+ }
853
+ }
792
854
  withAttachments(task, context) {
793
855
  if (!this.attachmentContext)
794
856
  return { task, context };
@@ -1385,12 +1447,13 @@ ${hubContextStr}
1385
1447
  while (iterations < maxIterations) {
1386
1448
  iterations++;
1387
1449
  const toolChoice = forcedToolChoice ?? this.getToolChoice(currentMessages);
1450
+ const requestMaxTokens = wantsTodoOrCal ? this.toolMaxTokens : this.maxTokens;
1388
1451
  const requestStart = Date.now();
1389
1452
  console.log(`[ORCHESTRATOR] Anthropic request ${iterations}/${maxIterations} (model: ${this.model})`);
1390
1453
  const response = await Promise.race([
1391
1454
  this.client.messages.create({
1392
1455
  model: this.model,
1393
- max_tokens: this.maxTokens,
1456
+ max_tokens: requestMaxTokens,
1394
1457
  system,
1395
1458
  tools,
1396
1459
  messages: currentMessages,
@@ -1403,6 +1466,11 @@ ${hubContextStr}
1403
1466
  })
1404
1467
  ]);
1405
1468
  console.log(`[ORCHESTRATOR] Anthropic response ${iterations}/${maxIterations} in ${Date.now() - requestStart}ms`);
1469
+ this.recordUsage(response, {
1470
+ iterations,
1471
+ messages: currentMessages.length,
1472
+ toolChoice: toolChoice?.name
1473
+ });
1406
1474
  // Check if done
1407
1475
  if (response.stop_reason === 'end_turn' || !this.hasToolUse(response.content)) {
1408
1476
  if (forcedToolChoice && !retriedForcedTool) {
@@ -2347,7 +2415,9 @@ Unlike lia_plan (internal only), this creates a VISIBLE todo list that appears i
2347
2415
  };
2348
2416
  // Escape single quotes in prompt for shell safety
2349
2417
  const escapedPrompt = prompt.replace(/'/g, "'\\''");
2350
- const claudeCmd = `${this.claudePath} -p '${escapedPrompt}' --output-format text --dangerously-skip-permissions`;
2418
+ const budget = resolveClaudeBudgetUsd();
2419
+ const budgetArg = budget ? ` --max-budget-usd ${budget}` : '';
2420
+ const claudeCmd = `${this.claudePath} -p '${escapedPrompt}' --output-format text --dangerously-skip-permissions --model ${this.model}${budgetArg}`;
2351
2421
  const isRoot = process.getuid?.() === 0;
2352
2422
  const shellCmd = isRoot
2353
2423
  ? `runuser -u lia -m -- /bin/bash -l -c "${claudeCmd.replace(/"/g, '\\"')}"`
@@ -12,7 +12,7 @@ export declare const WORKER_IDENTITY = "## Worker Identity\nYou are a Claude Cod
12
12
  /**
13
13
  * Output format workers should follow
14
14
  */
15
- export declare const WORKER_OUTPUT_FORMAT = "## Output Format\nWhen done, clearly list:\n- Files created: /path/to/file.ext\n- Files modified: /path/to/file.ext\n- Key findings: brief summary\n\nBe concise. Execute efficiently.";
15
+ export declare const WORKER_OUTPUT_FORMAT = "## Output Format\nWhen done, clearly list:\n- Files created: /path/to/file.ext\n- Files modified: /path/to/file.ext\n- Key findings: 1-3 short bullets\n\nBe concise. Do not restate the task.";
16
16
  /**
17
17
  * Instructions for workers to contribute back to memory
18
18
  * Workers can add to orchestrator's memory using these patterns
@@ -44,9 +44,9 @@ export const WORKER_OUTPUT_FORMAT = `## Output Format
44
44
  When done, clearly list:
45
45
  - Files created: /path/to/file.ext
46
46
  - Files modified: /path/to/file.ext
47
- - Key findings: brief summary
47
+ - Key findings: 1-3 short bullets
48
48
 
49
- Be concise. Execute efficiently.`;
49
+ Be concise. Do not restate the task.`;
50
50
  /**
51
51
  * Instructions for workers to contribute back to memory
52
52
  * Workers can add to orchestrator's memory using these patterns
@@ -71,6 +71,15 @@ Share data with other workers:
71
71
  */
72
72
  export function getWorkerLoggingInstructions(jobId) {
73
73
  const logFile = `/tmp/worker-${jobId}-log.txt`;
74
+ const verbose = process.env.LIA_WORKER_VERBOSE === '1';
75
+ if (!verbose) {
76
+ return `## Progress & Logging
77
+ - Keep output minimal; report only key steps and final results
78
+
79
+ REQUIRED - Log Export:
80
+ At the END of your work, create a final log file at: ${logFile}
81
+ Include: job_id=${jobId}, timestamp, summary of work done, files modified, key findings.`;
82
+ }
74
83
  return `## Progress & Logging
75
84
  - Output findings as you go, don't wait until the end
76
85
  - Print discoveries and insights immediately as you find them
@@ -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
  }
@@ -48,6 +48,28 @@ function getFileType(ext) {
48
48
  return 'text';
49
49
  return 'other';
50
50
  }
51
+ const DEFAULT_CLAUDE_MODEL = 'claude-opus-4-5-20251101';
52
+ function readBooleanEnv(value, fallback) {
53
+ if (value === undefined)
54
+ return fallback;
55
+ return !['0', 'false', 'no', 'off'].includes(value.trim().toLowerCase());
56
+ }
57
+ function resolveClaudeModel() {
58
+ const raw = (process.env.LIA_MODEL || process.env.ANTHROPIC_MODEL || DEFAULT_CLAUDE_MODEL).trim();
59
+ if (!raw)
60
+ return DEFAULT_CLAUDE_MODEL;
61
+ if (readBooleanEnv(process.env.LIA_FORCE_OPUS, true) && raw !== DEFAULT_CLAUDE_MODEL) {
62
+ return DEFAULT_CLAUDE_MODEL;
63
+ }
64
+ return raw;
65
+ }
66
+ function resolveClaudeBudgetUsd() {
67
+ const raw = process.env.LIA_MAX_BUDGET_USD || process.env.CLAUDE_MAX_BUDGET_USD;
68
+ if (!raw)
69
+ return null;
70
+ const trimmed = raw.trim();
71
+ return trimmed ? trimmed : null;
72
+ }
51
73
  export class WorkerManager {
52
74
  config;
53
75
  activeWorkers = new Map();
@@ -166,22 +188,30 @@ export class WorkerManager {
166
188
  try {
167
189
  // Add checkpoint and logging instructions to prevent data loss
168
190
  const logFile = `/tmp/worker-${jobId}-log.txt`;
169
- const enhancedTask = `${task}
170
-
171
- IMPORTANT - Progress & Logging:
191
+ const verbose = process.env.LIA_WORKER_VERBOSE === '1';
192
+ const loggingBlock = verbose
193
+ ? `IMPORTANT - Progress & Logging:
172
194
  - Output findings as you go, don't wait until the end
173
195
  - Print discoveries and insights immediately as you find them
174
196
  - Report on each file/step before moving to the next
175
- - Do NOT open browsers or start servers - just create files and report paths
197
+ - Do NOT open browsers or start servers - just create files and report paths`
198
+ : `IMPORTANT - Progress & Logging:
199
+ - Keep output minimal; report only key steps and final results
200
+ - Do NOT open browsers or start servers - just create files and report paths`;
201
+ const enhancedTask = `${task}
202
+
203
+ ${loggingBlock}
176
204
 
177
205
  REQUIRED - Log Export:
178
206
  At the END of your work, create a final log file at: ${logFile}
179
- Include: job_id=${jobId}, timestamp, summary of work done, files modified, key findings.
180
- This ensures nothing is lost even if your output gets truncated.`;
207
+ Include: job_id=${jobId}, timestamp, summary of work done, files modified, key findings.`;
181
208
  // Escape single quotes in task for shell safety
182
209
  const escapedTask = enhancedTask.replace(/'/g, "'\\''");
183
210
  // Build the claude command
184
- const claudeCmd = `claude -p '${escapedTask}' --output-format text --dangerously-skip-permissions`;
211
+ const model = resolveClaudeModel();
212
+ const budget = resolveClaudeBudgetUsd();
213
+ const budgetArg = budget ? ` --max-budget-usd ${budget}` : '';
214
+ const claudeCmd = `claude -p '${escapedTask}' --output-format text --dangerously-skip-permissions --model ${model}${budgetArg}`;
185
215
  // If running as root, run workers as 'lia' user to avoid --dangerously-skip-permissions being blocked
186
216
  // The Claude CLI blocks this flag for root users as a security measure
187
217
  const isRoot = process.getuid?.() === 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siftd/connect-agent",
3
- "version": "0.2.51",
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",