@siftd/connect-agent 0.2.19 → 0.2.20
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/dist/agent.js +14 -6
- package/dist/heartbeat.js +1 -1
- package/dist/orchestrator.d.ts +7 -2
- package/dist/orchestrator.js +81 -179
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -181,9 +181,9 @@ export async function processMessage(message) {
|
|
|
181
181
|
);
|
|
182
182
|
return response;
|
|
183
183
|
}
|
|
184
|
-
//
|
|
185
|
-
if (content
|
|
186
|
-
console.log('[AGENT]
|
|
184
|
+
// System command: force update (sent by webapp banner)
|
|
185
|
+
if (content === '/system-update') {
|
|
186
|
+
console.log('[AGENT] === SYSTEM UPDATE COMMAND ===');
|
|
187
187
|
return await performSelfUpdate();
|
|
188
188
|
}
|
|
189
189
|
try {
|
|
@@ -245,16 +245,24 @@ export async function runAgent(pollInterval = 2000) {
|
|
|
245
245
|
// Try WebSocket first
|
|
246
246
|
wsClient = new AgentWebSocket();
|
|
247
247
|
const wsConnected = await wsClient.connect();
|
|
248
|
-
//
|
|
248
|
+
// Set up worker callbacks
|
|
249
249
|
if (orchestrator) {
|
|
250
|
+
// Progress bars
|
|
250
251
|
orchestrator.setWorkerStatusCallback((workers) => {
|
|
251
252
|
if (wsClient?.connected()) {
|
|
252
253
|
wsClient.sendWorkersUpdate(workers);
|
|
253
254
|
}
|
|
254
|
-
// Log running workers count for visibility even without WebSocket
|
|
255
255
|
const running = workers.filter(w => w.status === 'running');
|
|
256
256
|
if (running.length > 0) {
|
|
257
|
-
console.log(`[WORKERS] ${running.length} running
|
|
257
|
+
console.log(`[WORKERS] ${running.length} running`);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
// Worker results - send to user when workers complete
|
|
261
|
+
orchestrator.setWorkerResultCallback((workerId, result) => {
|
|
262
|
+
console.log(`[WORKER DONE] ${workerId}: ${result.slice(0, 100)}...`);
|
|
263
|
+
if (wsClient?.connected()) {
|
|
264
|
+
// Send as response with worker ID as message ID
|
|
265
|
+
wsClient.sendResponse(workerId, `**Worker completed:**\n\n${result}`);
|
|
258
266
|
}
|
|
259
267
|
});
|
|
260
268
|
}
|
package/dist/heartbeat.js
CHANGED
|
@@ -10,7 +10,7 @@ import { hostname } from 'os';
|
|
|
10
10
|
import { createHash } from 'crypto';
|
|
11
11
|
import { getServerUrl, getAgentToken, getUserId, isCloudMode } from './config.js';
|
|
12
12
|
const HEARTBEAT_INTERVAL = 10000; // 10 seconds
|
|
13
|
-
const VERSION = '0.2.
|
|
13
|
+
const VERSION = '0.2.20'; // Should match package.json
|
|
14
14
|
const state = {
|
|
15
15
|
intervalId: null,
|
|
16
16
|
runnerId: null,
|
package/dist/orchestrator.d.ts
CHANGED
|
@@ -101,9 +101,14 @@ export declare class MasterOrchestrator {
|
|
|
101
101
|
* Process tool calls
|
|
102
102
|
*/
|
|
103
103
|
private processToolCalls;
|
|
104
|
+
private workerResultCallback;
|
|
104
105
|
/**
|
|
105
|
-
*
|
|
106
|
-
|
|
106
|
+
* Set callback for when workers complete (for async notification)
|
|
107
|
+
*/
|
|
108
|
+
setWorkerResultCallback(callback: ((workerId: string, result: string) => void) | null): void;
|
|
109
|
+
/**
|
|
110
|
+
* Delegate task to Claude Code CLI worker - NON-BLOCKING
|
|
111
|
+
* Returns immediately, sends results via callback when done
|
|
107
112
|
*/
|
|
108
113
|
private delegateToWorker;
|
|
109
114
|
/**
|
package/dist/orchestrator.js
CHANGED
|
@@ -828,197 +828,99 @@ Be specific about what you want done.`,
|
|
|
828
828
|
}
|
|
829
829
|
return results;
|
|
830
830
|
}
|
|
831
|
+
// Callback for sending results when worker completes
|
|
832
|
+
workerResultCallback = null;
|
|
831
833
|
/**
|
|
832
|
-
*
|
|
833
|
-
* @param timeoutMs - Timeout in milliseconds (default: 30 minutes, max: 60 minutes)
|
|
834
|
+
* Set callback for when workers complete (for async notification)
|
|
834
835
|
*/
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
836
|
+
setWorkerResultCallback(callback) {
|
|
837
|
+
this.workerResultCallback = callback;
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Delegate task to Claude Code CLI worker - NON-BLOCKING
|
|
841
|
+
* Returns immediately, sends results via callback when done
|
|
842
|
+
*/
|
|
843
|
+
async delegateToWorker(task, context, workingDir) {
|
|
839
844
|
const id = `worker_${Date.now()}_${++this.jobCounter}`;
|
|
840
845
|
const cwd = workingDir || this.workspaceDir;
|
|
841
|
-
//
|
|
842
|
-
let memoryContext = '';
|
|
843
|
-
try {
|
|
844
|
-
const relevantMemories = await this.memory.search(task, { limit: 5, minImportance: 0.3 });
|
|
845
|
-
if (relevantMemories.length > 0) {
|
|
846
|
-
memoryContext = '\n\nRELEVANT KNOWLEDGE FROM MEMORY:\n' +
|
|
847
|
-
relevantMemories.map(m => `[${m.type}] ${m.content}`).join('\n');
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
catch (error) {
|
|
851
|
-
// Memory search failed, continue without it
|
|
852
|
-
console.log('[ORCHESTRATOR] Memory search failed, continuing without context');
|
|
853
|
-
}
|
|
854
|
-
// Get shared state context for worker coordination
|
|
855
|
-
const sharedContext = this.sharedState.getSummaryForWorker(id);
|
|
856
|
-
// Build prompt for worker with memory context and checkpoint instructions
|
|
846
|
+
// Build simple prompt - no bloat
|
|
857
847
|
let prompt = task;
|
|
858
848
|
if (context) {
|
|
859
849
|
prompt = `Context: ${context}\n\nTask: ${task}`;
|
|
860
850
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
prompt += `\n\nWORKER COORDINATION:${sharedContext}`;
|
|
866
|
-
}
|
|
867
|
-
// Add checkpoint and logging instructions to prevent data loss
|
|
868
|
-
const logFile = `/tmp/worker-${id}-log.txt`;
|
|
869
|
-
prompt += `
|
|
870
|
-
|
|
871
|
-
IMPORTANT - Progress & Logging:
|
|
872
|
-
- Output findings as you go, don't wait until the end
|
|
873
|
-
- Print discoveries, file paths, and insights immediately as you find them
|
|
874
|
-
- Report on each file/step before moving to the next
|
|
875
|
-
|
|
876
|
-
REQUIRED - Log Export:
|
|
877
|
-
At the END of your work, create a final log file at: ${logFile}
|
|
878
|
-
Include: job_id=${id}, timestamp, summary of work done, files modified, key findings.
|
|
879
|
-
This ensures nothing is lost even if your output gets truncated.
|
|
880
|
-
|
|
881
|
-
LEARNING EXPORT (optional but valuable):
|
|
882
|
-
If you discover something important (patterns, user preferences, technical insights),
|
|
883
|
-
end your response with a line like:
|
|
884
|
-
[MEMORY] type=semantic | content=User prefers X over Y for this type of task
|
|
885
|
-
[MEMORY] type=procedural | content=When doing X, always check Y first
|
|
886
|
-
This helps me remember and improve for future tasks.
|
|
887
|
-
|
|
888
|
-
WORKER COORDINATION (optional):
|
|
889
|
-
If you need to share data with other workers or signal completion:
|
|
890
|
-
[SHARE] key=myData | value={"result": "something useful"}
|
|
891
|
-
[SIGNAL] name=step1_complete | data={"files": ["a.ts", "b.ts"]}
|
|
892
|
-
[MESSAGE] to=worker_xyz | content=Please review the changes I made
|
|
893
|
-
This enables parallel workers to coordinate.`;
|
|
894
|
-
console.log(`[ORCHESTRATOR] Delegating to worker ${id}: ${task.slice(0, 80)}...`);
|
|
895
|
-
// Estimate task duration based on content
|
|
851
|
+
// Add brief instruction
|
|
852
|
+
prompt += `\n\nBe concise. Output results directly.`;
|
|
853
|
+
console.log(`[ORCHESTRATOR] Worker ${id} starting: ${task.slice(0, 80)}...`);
|
|
854
|
+
// Estimate task duration
|
|
896
855
|
const estimatedTime = this.estimateTaskDuration(task);
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
const timeoutMinutes = Math.round(workerTimeout / 60000);
|
|
920
|
-
const timeout = setTimeout(async () => {
|
|
921
|
-
if (job.status === 'running') {
|
|
922
|
-
job.status = 'timeout';
|
|
923
|
-
job.endTime = Date.now();
|
|
924
|
-
const duration = Math.round((job.endTime - job.startTime) / 1000);
|
|
925
|
-
// Send SIGINT first to allow graceful shutdown and output flush
|
|
926
|
-
child.kill('SIGINT');
|
|
927
|
-
// Give 5 seconds for graceful shutdown before SIGTERM
|
|
928
|
-
setTimeout(() => {
|
|
929
|
-
if (!child.killed) {
|
|
930
|
-
child.kill('SIGTERM');
|
|
931
|
-
}
|
|
932
|
-
}, 5000);
|
|
933
|
-
// Wait a moment for graceful shutdown to flush logs
|
|
934
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
935
|
-
const partialOutput = job.output.trim();
|
|
936
|
-
// Try to recover from log file
|
|
937
|
-
const recoveredLog = this.recoverWorkerLog(id);
|
|
938
|
-
// Log failure to memory for learning
|
|
939
|
-
await this.logWorkerFailure(id, task, `Timeout after ${timeoutMinutes} minutes`, duration, recoveredLog || undefined);
|
|
940
|
-
// Build combined output
|
|
941
|
-
let combinedOutput = '';
|
|
942
|
-
if (recoveredLog) {
|
|
943
|
-
combinedOutput = `[Recovered from log file]\n${recoveredLog}\n\n`;
|
|
944
|
-
}
|
|
945
|
-
if (partialOutput) {
|
|
946
|
-
combinedOutput += `[Partial stdout]\n${partialOutput.slice(-3000)}`;
|
|
947
|
-
}
|
|
948
|
-
resolve({
|
|
949
|
-
success: false,
|
|
950
|
-
output: combinedOutput
|
|
951
|
-
? `Worker timed out after ${timeoutMinutes} minutes. Recovered output:\n${combinedOutput}`
|
|
952
|
-
: `Worker timed out after ${timeoutMinutes} minutes with no recoverable output.`
|
|
953
|
-
});
|
|
954
|
-
}
|
|
955
|
-
}, workerTimeout);
|
|
956
|
-
child.stdout?.on('data', (data) => {
|
|
957
|
-
job.output += data.toString();
|
|
958
|
-
});
|
|
959
|
-
child.stderr?.on('data', (data) => {
|
|
960
|
-
// Ignore status messages
|
|
961
|
-
const text = data.toString();
|
|
962
|
-
if (!text.includes('Checking') && !text.includes('Connected')) {
|
|
963
|
-
job.output += text;
|
|
964
|
-
}
|
|
965
|
-
});
|
|
966
|
-
child.on('close', async (code) => {
|
|
967
|
-
clearTimeout(timeout);
|
|
968
|
-
job.status = code === 0 ? 'completed' : 'failed';
|
|
969
|
-
job.endTime = Date.now();
|
|
970
|
-
const duration = Math.round((job.endTime - job.startTime) / 1000);
|
|
971
|
-
console.log(`[ORCHESTRATOR] Worker ${id} finished in ${duration}s (code: ${code})`);
|
|
972
|
-
let finalOutput = job.output.trim();
|
|
973
|
-
if (code !== 0 || finalOutput.length === 0) {
|
|
974
|
-
console.log(`[ORCHESTRATOR] Worker ${id} output: ${finalOutput.slice(0, 200) || '(empty)'}`);
|
|
975
|
-
// Try to recover from log file on failure or empty output
|
|
976
|
-
const recoveredLog = this.recoverWorkerLog(id);
|
|
977
|
-
if (recoveredLog) {
|
|
978
|
-
finalOutput = recoveredLog + (finalOutput ? `\n\n[Additional stdout]\n${finalOutput}` : '');
|
|
979
|
-
}
|
|
980
|
-
// Log failure to memory
|
|
981
|
-
if (code !== 0) {
|
|
982
|
-
await this.logWorkerFailure(id, task, `Exit code ${code}`, duration, recoveredLog || undefined);
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
// Extract and store memory contributions from worker output
|
|
986
|
-
await this.extractWorkerMemories(finalOutput, id);
|
|
987
|
-
resolve({
|
|
988
|
-
success: code === 0,
|
|
989
|
-
output: finalOutput || '(No output)'
|
|
990
|
-
});
|
|
991
|
-
});
|
|
992
|
-
child.on('error', async (err) => {
|
|
993
|
-
clearTimeout(timeout);
|
|
994
|
-
job.status = 'failed';
|
|
856
|
+
const job = {
|
|
857
|
+
id,
|
|
858
|
+
task: task.slice(0, 200),
|
|
859
|
+
status: 'running',
|
|
860
|
+
startTime: Date.now(),
|
|
861
|
+
output: '',
|
|
862
|
+
estimatedTime
|
|
863
|
+
};
|
|
864
|
+
// Escape single quotes in prompt for shell safety
|
|
865
|
+
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
|
866
|
+
// Spawn worker
|
|
867
|
+
const child = spawn('/bin/bash', ['-l', '-c', `claude -p '${escapedPrompt}' --dangerously-skip-permissions`], {
|
|
868
|
+
cwd,
|
|
869
|
+
env: { ...process.env },
|
|
870
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
871
|
+
});
|
|
872
|
+
job.process = child;
|
|
873
|
+
this.jobs.set(id, job);
|
|
874
|
+
// 5 minute timeout
|
|
875
|
+
const timeout = setTimeout(() => {
|
|
876
|
+
if (job.status === 'running') {
|
|
877
|
+
job.status = 'timeout';
|
|
995
878
|
job.endTime = Date.now();
|
|
996
|
-
|
|
997
|
-
console.
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
const isRetryable = retryableErrors.some(code => err.message.includes(code));
|
|
1001
|
-
if (isRetryable && retryCount < maxRetries) {
|
|
1002
|
-
console.log(`[ORCHESTRATOR] Retrying worker (attempt ${retryCount + 2}/${maxRetries + 1}) after ${err.message}...`);
|
|
1003
|
-
// Exponential backoff: 500ms, 1000ms, 2000ms
|
|
1004
|
-
const delay = 500 * Math.pow(2, retryCount);
|
|
1005
|
-
await new Promise(r => setTimeout(r, delay));
|
|
1006
|
-
const retryResult = await this.delegateToWorker(task, context, workingDir, retryCount + 1, timeoutMs);
|
|
1007
|
-
resolve(retryResult);
|
|
1008
|
-
return;
|
|
879
|
+
child.kill('SIGTERM');
|
|
880
|
+
console.log(`[ORCHESTRATOR] Worker ${id} timed out`);
|
|
881
|
+
if (this.workerResultCallback) {
|
|
882
|
+
this.workerResultCallback(id, `Worker timed out. Partial output: ${job.output.slice(-1000) || 'none'}`);
|
|
1009
883
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
884
|
+
}
|
|
885
|
+
}, 5 * 60 * 1000);
|
|
886
|
+
child.stdout?.on('data', (data) => {
|
|
887
|
+
const text = data.toString();
|
|
888
|
+
job.output += text;
|
|
889
|
+
// Stream output to console
|
|
890
|
+
process.stdout.write(`[${id}] ${text}`);
|
|
891
|
+
});
|
|
892
|
+
child.stderr?.on('data', (data) => {
|
|
893
|
+
const text = data.toString();
|
|
894
|
+
if (!text.includes('Checking') && !text.includes('Connected')) {
|
|
895
|
+
job.output += text;
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
child.on('close', async (code) => {
|
|
899
|
+
clearTimeout(timeout);
|
|
900
|
+
job.status = code === 0 ? 'completed' : 'failed';
|
|
901
|
+
job.endTime = Date.now();
|
|
902
|
+
const duration = Math.round((job.endTime - job.startTime) / 1000);
|
|
903
|
+
console.log(`[ORCHESTRATOR] Worker ${id} done in ${duration}s`);
|
|
904
|
+
const result = job.output.trim() || '(No output)';
|
|
905
|
+
// Notify via callback (sends to user via WebSocket)
|
|
906
|
+
if (this.workerResultCallback) {
|
|
907
|
+
this.workerResultCallback(id, result);
|
|
908
|
+
}
|
|
1021
909
|
});
|
|
910
|
+
child.on('error', (err) => {
|
|
911
|
+
clearTimeout(timeout);
|
|
912
|
+
job.status = 'failed';
|
|
913
|
+
job.endTime = Date.now();
|
|
914
|
+
console.error(`[ORCHESTRATOR] Worker ${id} error:`, err.message);
|
|
915
|
+
if (this.workerResultCallback) {
|
|
916
|
+
this.workerResultCallback(id, `Worker error: ${err.message}`);
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
// Return immediately - worker runs in background
|
|
920
|
+
return {
|
|
921
|
+
success: true,
|
|
922
|
+
output: `Worker ${id} started. I'll notify you when it completes.`
|
|
923
|
+
};
|
|
1022
924
|
}
|
|
1023
925
|
/**
|
|
1024
926
|
* Try to recover worker output from log file
|