@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 CHANGED
@@ -181,9 +181,9 @@ export async function processMessage(message) {
181
181
  );
182
182
  return response;
183
183
  }
184
- // Handle self-update requests - trigger on "update" keyword
185
- if (content.includes('update') && (content.includes('agent') || content.includes('yourself') || content.includes('latest'))) {
186
- console.log('[AGENT] Update request detected');
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
- // Always set up worker status callback for progress bars (works with or without WebSocket)
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: ${running.map(w => `${w.id} (${w.progress}%)`).join(', ')}`);
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.19'; // Should match package.json
13
+ const VERSION = '0.2.20'; // Should match package.json
14
14
  const state = {
15
15
  intervalId: null,
16
16
  runnerId: null,
@@ -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
- * Delegate task to Claude Code CLI worker with retry logic
106
- * @param timeoutMs - Timeout in milliseconds (default: 30 minutes, max: 60 minutes)
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
  /**
@@ -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
- * Delegate task to Claude Code CLI worker with retry logic
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
- async delegateToWorker(task, context, workingDir, retryCount = 0, timeoutMs) {
836
- const maxRetries = 2;
837
- // Default 30 min, max 60 min
838
- const workerTimeout = Math.min(timeoutMs || 30 * 60 * 1000, 60 * 60 * 1000);
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
- // Search for relevant memories to inject into worker prompt
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
- if (memoryContext) {
862
- prompt += memoryContext;
863
- }
864
- if (sharedContext) {
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
- return new Promise((resolve) => {
898
- const job = {
899
- id,
900
- task: task.slice(0, 200),
901
- status: 'running',
902
- startTime: Date.now(),
903
- output: '',
904
- estimatedTime
905
- };
906
- // Escape single quotes in prompt for shell safety
907
- // Replace ' with '\'' (end quote, escaped quote, start quote)
908
- const escapedPrompt = prompt.replace(/'/g, "'\\''");
909
- // Spawn using /bin/bash -l -c to ensure proper PATH resolution (NVM, etc.)
910
- // Use absolute path to bash to avoid ENOENT errors in restricted environments
911
- const child = spawn('/bin/bash', ['-l', '-c', `claude -p '${escapedPrompt}' --dangerously-skip-permissions`], {
912
- cwd,
913
- env: { ...process.env },
914
- stdio: ['pipe', 'pipe', 'pipe']
915
- });
916
- job.process = child;
917
- this.jobs.set(id, job);
918
- // Configurable timeout (default 30 min, max 60 min)
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
- const duration = Math.round((job.endTime - job.startTime) / 1000);
997
- console.error(`[ORCHESTRATOR] Worker ${id} spawn error:`, err.message);
998
- // Retryable errors: ENOENT (command not found), EAGAIN (resource busy), ENOMEM (out of memory)
999
- const retryableErrors = ['ENOENT', 'EAGAIN', 'ENOMEM', 'ETIMEDOUT', 'ECONNRESET'];
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
- // Log failure to memory
1011
- await this.logWorkerFailure(id, task, err.message, duration);
1012
- // Try to recover any partial output from log file
1013
- const recoveredLog = this.recoverWorkerLog(id);
1014
- resolve({
1015
- success: false,
1016
- output: recoveredLog
1017
- ? `Worker error: ${err.message}\n\n[Recovered from log]\n${recoveredLog}`
1018
- : `Worker error: ${err.message}`
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siftd/connect-agent",
3
- "version": "0.2.19",
3
+ "version": "0.2.20",
4
4
  "description": "Master orchestrator agent - control Claude Code remotely via web",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",