@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 +16 -0
- package/dist/agent.js +63 -35
- package/dist/api.js +3 -1
- package/dist/orchestrator.d.ts +2 -0
- package/dist/orchestrator.js +75 -5
- package/dist/prompts/worker-system.d.ts +1 -1
- package/dist/prompts/worker-system.js +11 -2
- package/dist/websocket.d.ts +2 -0
- package/dist/websocket.js +37 -6
- package/dist/workers/manager.js +37 -7
- package/package.json +1 -1
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
441
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
474
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/orchestrator.d.ts
CHANGED
|
@@ -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;
|
package/dist/orchestrator.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
47
|
+
- Key findings: 1-3 short bullets
|
|
48
48
|
|
|
49
|
-
Be concise.
|
|
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
|
package/dist/websocket.d.ts
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
+
finalize(false);
|
|
96
124
|
}
|
|
97
125
|
}, 10000);
|
|
98
126
|
}
|
|
99
127
|
catch (error) {
|
|
100
128
|
console.error('[WS] Connection failed:', error);
|
|
101
|
-
|
|
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.
|
|
371
|
+
if (!this.connecting) {
|
|
372
|
+
this.connect();
|
|
373
|
+
}
|
|
343
374
|
}, delay);
|
|
344
375
|
}
|
|
345
376
|
}
|
package/dist/workers/manager.js
CHANGED
|
@@ -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
|
|
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
|
|
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;
|