@siftd/connect-agent 0.2.19 → 0.2.21

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
  }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Hub - The Agent's Brain Home
3
+ *
4
+ * Manages structured memory files in ~/.connect-hub/
5
+ * Human-readable markdown files that the orchestrator reads/writes.
6
+ */
7
+ export interface HubContext {
8
+ agentIdentity: string;
9
+ landmarks: string;
10
+ projectBio: string | null;
11
+ projectName: string | null;
12
+ }
13
+ /**
14
+ * Ensure hub directory structure exists
15
+ */
16
+ export declare function ensureHubExists(): void;
17
+ /**
18
+ * Load hub context for a session
19
+ * Reads AGENTS.md, LANDMARKS.md, and relevant project bio
20
+ */
21
+ export declare function loadHubContext(message?: string): HubContext;
22
+ /**
23
+ * Format hub context for inclusion in system prompt
24
+ */
25
+ export declare function formatHubContext(ctx: HubContext): string;
26
+ /**
27
+ * Append to action log
28
+ */
29
+ export declare function logAction(action: string, project?: string, details?: string): void;
30
+ /**
31
+ * Update or create a project bio
32
+ */
33
+ export declare function updateProjectBio(projectName: string, content: string): void;
34
+ /**
35
+ * Append learning to a project bio
36
+ */
37
+ export declare function appendToProjectBio(projectName: string, section: string, content: string): void;
38
+ /**
39
+ * Log a mistake for learning
40
+ */
41
+ export declare function logMistake(what: string, rootCause: string, fix: string, lesson: string): void;
42
+ /**
43
+ * Update LANDMARKS.md with current state
44
+ */
45
+ export declare function updateLandmarks(content: string): void;
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Hub - The Agent's Brain Home
3
+ *
4
+ * Manages structured memory files in ~/.connect-hub/
5
+ * Human-readable markdown files that the orchestrator reads/writes.
6
+ */
7
+ import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { homedir } from 'os';
10
+ const HUB_DIR = join(homedir(), '.connect-hub');
11
+ const PROJECTS_DIR = join(HUB_DIR, 'projects');
12
+ /**
13
+ * Ensure hub directory structure exists
14
+ */
15
+ export function ensureHubExists() {
16
+ if (!existsSync(HUB_DIR)) {
17
+ mkdirSync(HUB_DIR, { recursive: true });
18
+ }
19
+ if (!existsSync(PROJECTS_DIR)) {
20
+ mkdirSync(PROJECTS_DIR, { recursive: true });
21
+ }
22
+ }
23
+ /**
24
+ * Read a hub file safely
25
+ */
26
+ function readHubFile(filename) {
27
+ const path = join(HUB_DIR, filename);
28
+ if (existsSync(path)) {
29
+ try {
30
+ return readFileSync(path, 'utf-8');
31
+ }
32
+ catch (e) {
33
+ console.error(`[HUB] Failed to read ${filename}:`, e);
34
+ return null;
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+ /**
40
+ * Read a project bio
41
+ */
42
+ function readProjectBio(projectName) {
43
+ // Normalize project name (lowercase, hyphens)
44
+ const normalized = projectName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
45
+ const path = join(PROJECTS_DIR, `${normalized}.md`);
46
+ if (existsSync(path)) {
47
+ try {
48
+ return readFileSync(path, 'utf-8');
49
+ }
50
+ catch (e) {
51
+ console.error(`[HUB] Failed to read project bio ${normalized}:`, e);
52
+ return null;
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+ /**
58
+ * Detect project name from message content
59
+ */
60
+ function detectProject(message) {
61
+ // Common project patterns
62
+ const patterns = [
63
+ /\b(connect[-_]?app[-_]?2?)\b/i,
64
+ /\b(lia[-_]?live)\b/i,
65
+ /\b(game[-_]?001)\b/i,
66
+ /\bproj(?:ect)?[-_:]?\s*([a-z0-9-]+)/i,
67
+ /\bin\s+([a-z0-9-]+)\s+(?:project|repo|codebase)/i,
68
+ ];
69
+ for (const pattern of patterns) {
70
+ const match = message.match(pattern);
71
+ if (match) {
72
+ return match[1].toLowerCase().replace(/[^a-z0-9-]/g, '-');
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+ /**
78
+ * Load hub context for a session
79
+ * Reads AGENTS.md, LANDMARKS.md, and relevant project bio
80
+ */
81
+ export function loadHubContext(message) {
82
+ ensureHubExists();
83
+ const agentIdentity = readHubFile('AGENTS.md') || '';
84
+ const landmarks = readHubFile('LANDMARKS.md') || '';
85
+ let projectBio = null;
86
+ let projectName = null;
87
+ // Try to detect project from message
88
+ if (message) {
89
+ projectName = detectProject(message);
90
+ if (projectName) {
91
+ projectBio = readProjectBio(projectName);
92
+ }
93
+ }
94
+ return {
95
+ agentIdentity,
96
+ landmarks,
97
+ projectBio,
98
+ projectName
99
+ };
100
+ }
101
+ /**
102
+ * Format hub context for inclusion in system prompt
103
+ */
104
+ export function formatHubContext(ctx) {
105
+ const parts = [];
106
+ if (ctx.landmarks) {
107
+ parts.push(`## Current State (LANDMARKS)\n\n${ctx.landmarks}`);
108
+ }
109
+ if (ctx.projectBio && ctx.projectName) {
110
+ parts.push(`## Project Context: ${ctx.projectName}\n\n${ctx.projectBio}`);
111
+ }
112
+ return parts.join('\n\n---\n\n');
113
+ }
114
+ /**
115
+ * Append to action log
116
+ */
117
+ export function logAction(action, project, details) {
118
+ ensureHubExists();
119
+ const logPath = join(HUB_DIR, 'ACTION-LOG.md');
120
+ const now = new Date();
121
+ const timestamp = now.toISOString().slice(0, 16).replace('T', ' ');
122
+ const projectPart = project ? ` — ${project}` : '';
123
+ const detailsPart = details ? ` — ${details}` : '';
124
+ const entry = `${timestamp}${projectPart} — ${action}${detailsPart}\n`;
125
+ try {
126
+ appendFileSync(logPath, entry);
127
+ }
128
+ catch (e) {
129
+ console.error('[HUB] Failed to write action log:', e);
130
+ }
131
+ }
132
+ /**
133
+ * Update or create a project bio
134
+ */
135
+ export function updateProjectBio(projectName, content) {
136
+ ensureHubExists();
137
+ const normalized = projectName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
138
+ const path = join(PROJECTS_DIR, `${normalized}.md`);
139
+ try {
140
+ writeFileSync(path, content);
141
+ console.log(`[HUB] Updated project bio: ${normalized}`);
142
+ }
143
+ catch (e) {
144
+ console.error(`[HUB] Failed to write project bio ${normalized}:`, e);
145
+ }
146
+ }
147
+ /**
148
+ * Append learning to a project bio
149
+ */
150
+ export function appendToProjectBio(projectName, section, content) {
151
+ ensureHubExists();
152
+ const normalized = projectName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
153
+ const path = join(PROJECTS_DIR, `${normalized}.md`);
154
+ try {
155
+ let existing = '';
156
+ if (existsSync(path)) {
157
+ existing = readFileSync(path, 'utf-8');
158
+ }
159
+ else {
160
+ existing = `# Project: ${projectName}\n\n`;
161
+ }
162
+ // Append under the section
163
+ const sectionHeader = `## ${section}`;
164
+ if (existing.includes(sectionHeader)) {
165
+ // Append to existing section
166
+ const parts = existing.split(sectionHeader);
167
+ const afterSection = parts[1] || '';
168
+ const nextSectionIndex = afterSection.indexOf('\n## ');
169
+ if (nextSectionIndex > -1) {
170
+ // Insert before next section
171
+ const sectionContent = afterSection.slice(0, nextSectionIndex);
172
+ const rest = afterSection.slice(nextSectionIndex);
173
+ existing = parts[0] + sectionHeader + sectionContent + '\n' + content + '\n' + rest;
174
+ }
175
+ else {
176
+ // Append at end
177
+ existing = existing + '\n' + content + '\n';
178
+ }
179
+ }
180
+ else {
181
+ // Create new section
182
+ existing = existing + `\n${sectionHeader}\n\n${content}\n`;
183
+ }
184
+ writeFileSync(path, existing);
185
+ }
186
+ catch (e) {
187
+ console.error(`[HUB] Failed to append to project bio ${normalized}:`, e);
188
+ }
189
+ }
190
+ /**
191
+ * Log a mistake for learning
192
+ */
193
+ export function logMistake(what, rootCause, fix, lesson) {
194
+ ensureHubExists();
195
+ const path = join(HUB_DIR, 'MISTAKES.md');
196
+ const now = new Date();
197
+ const date = now.toISOString().slice(0, 10);
198
+ const entry = `
199
+ ---
200
+
201
+ ## ${date} — ${what}
202
+
203
+ **What happened**: ${what}
204
+
205
+ **Root cause**: ${rootCause}
206
+
207
+ **Fix**: ${fix}
208
+
209
+ **Lesson**: ${lesson}
210
+ `;
211
+ try {
212
+ appendFileSync(path, entry);
213
+ }
214
+ catch (e) {
215
+ console.error('[HUB] Failed to write mistake log:', e);
216
+ }
217
+ }
218
+ /**
219
+ * Update LANDMARKS.md with current state
220
+ */
221
+ export function updateLandmarks(content) {
222
+ ensureHubExists();
223
+ const path = join(HUB_DIR, 'LANDMARKS.md');
224
+ try {
225
+ writeFileSync(path, content);
226
+ console.log('[HUB] Updated LANDMARKS.md');
227
+ }
228
+ catch (e) {
229
+ console.error('[HUB] Failed to update LANDMARKS.md:', e);
230
+ }
231
+ }
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
  /**
@@ -16,6 +16,7 @@ import { WebTools } from './tools/web.js';
16
16
  import { WorkerTools } from './tools/worker.js';
17
17
  import { SharedState } from './workers/shared-state.js';
18
18
  import { getKnowledgeForPrompt } from './genesis/index.js';
19
+ import { loadHubContext, formatHubContext, logAction } from './core/hub.js';
19
20
  const SYSTEM_PROMPT = `You are a MASTER ORCHESTRATOR - NOT a worker. You delegate ALL file/code work to Claude Code CLI workers.
20
21
 
21
22
  CRITICAL IDENTITY:
@@ -331,11 +332,18 @@ export class MasterOrchestrator {
331
332
  if (slashResponse) {
332
333
  return slashResponse;
333
334
  }
335
+ // Load hub context (AGENTS.md identity, LANDMARKS.md state, project bio if relevant)
336
+ const hubContext = loadHubContext(message);
337
+ const hubContextStr = formatHubContext(hubContext);
334
338
  // Build context from memory
335
339
  const memoryContext = await this.getMemoryContext(message);
336
- // Build system prompt with genesis knowledge and memory context
340
+ // Build system prompt with hub context, genesis knowledge, and memory context
337
341
  const genesisKnowledge = getKnowledgeForPrompt();
338
342
  let systemWithContext = SYSTEM_PROMPT + genesisKnowledge;
343
+ // Add hub context (landmarks + project bio)
344
+ if (hubContextStr) {
345
+ systemWithContext += `\n\n---\n\nHUB CONTEXT:\n${hubContextStr}`;
346
+ }
339
347
  if (memoryContext) {
340
348
  systemWithContext += `\n\nRELEVANT MEMORIES:\n${memoryContext}`;
341
349
  }
@@ -348,6 +356,11 @@ export class MasterOrchestrator {
348
356
  const response = await this.runAgentLoop(messages, systemWithContext, sendMessage, apiKey);
349
357
  // Auto-remember important things from the conversation
350
358
  await this.autoRemember(message, response);
359
+ // Log significant actions to hub
360
+ if (response.length > 100) {
361
+ const action = message.length > 50 ? message.slice(0, 50) + '...' : message;
362
+ logAction(action, hubContext.projectName || undefined, `Response: ${response.length} chars`);
363
+ }
351
364
  return response;
352
365
  }
353
366
  catch (error) {
@@ -828,197 +841,99 @@ Be specific about what you want done.`,
828
841
  }
829
842
  return results;
830
843
  }
844
+ // Callback for sending results when worker completes
845
+ workerResultCallback = null;
846
+ /**
847
+ * Set callback for when workers complete (for async notification)
848
+ */
849
+ setWorkerResultCallback(callback) {
850
+ this.workerResultCallback = callback;
851
+ }
831
852
  /**
832
- * Delegate task to Claude Code CLI worker with retry logic
833
- * @param timeoutMs - Timeout in milliseconds (default: 30 minutes, max: 60 minutes)
853
+ * Delegate task to Claude Code CLI worker - NON-BLOCKING
854
+ * Returns immediately, sends results via callback when done
834
855
  */
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);
856
+ async delegateToWorker(task, context, workingDir) {
839
857
  const id = `worker_${Date.now()}_${++this.jobCounter}`;
840
858
  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
859
+ // Build simple prompt - no bloat
857
860
  let prompt = task;
858
861
  if (context) {
859
862
  prompt = `Context: ${context}\n\nTask: ${task}`;
860
863
  }
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
864
+ // Add brief instruction
865
+ prompt += `\n\nBe concise. Output results directly.`;
866
+ console.log(`[ORCHESTRATOR] Worker ${id} starting: ${task.slice(0, 80)}...`);
867
+ // Estimate task duration
896
868
  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';
869
+ const job = {
870
+ id,
871
+ task: task.slice(0, 200),
872
+ status: 'running',
873
+ startTime: Date.now(),
874
+ output: '',
875
+ estimatedTime
876
+ };
877
+ // Escape single quotes in prompt for shell safety
878
+ const escapedPrompt = prompt.replace(/'/g, "'\\''");
879
+ // Spawn worker
880
+ const child = spawn('/bin/bash', ['-l', '-c', `claude -p '${escapedPrompt}' --dangerously-skip-permissions`], {
881
+ cwd,
882
+ env: { ...process.env },
883
+ stdio: ['pipe', 'pipe', 'pipe']
884
+ });
885
+ job.process = child;
886
+ this.jobs.set(id, job);
887
+ // 5 minute timeout
888
+ const timeout = setTimeout(() => {
889
+ if (job.status === 'running') {
890
+ job.status = 'timeout';
995
891
  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;
892
+ child.kill('SIGTERM');
893
+ console.log(`[ORCHESTRATOR] Worker ${id} timed out`);
894
+ if (this.workerResultCallback) {
895
+ this.workerResultCallback(id, `Worker timed out. Partial output: ${job.output.slice(-1000) || 'none'}`);
1009
896
  }
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
- });
897
+ }
898
+ }, 5 * 60 * 1000);
899
+ child.stdout?.on('data', (data) => {
900
+ const text = data.toString();
901
+ job.output += text;
902
+ // Stream output to console
903
+ process.stdout.write(`[${id}] ${text}`);
904
+ });
905
+ child.stderr?.on('data', (data) => {
906
+ const text = data.toString();
907
+ if (!text.includes('Checking') && !text.includes('Connected')) {
908
+ job.output += text;
909
+ }
910
+ });
911
+ child.on('close', async (code) => {
912
+ clearTimeout(timeout);
913
+ job.status = code === 0 ? 'completed' : 'failed';
914
+ job.endTime = Date.now();
915
+ const duration = Math.round((job.endTime - job.startTime) / 1000);
916
+ console.log(`[ORCHESTRATOR] Worker ${id} done in ${duration}s`);
917
+ const result = job.output.trim() || '(No output)';
918
+ // Notify via callback (sends to user via WebSocket)
919
+ if (this.workerResultCallback) {
920
+ this.workerResultCallback(id, result);
921
+ }
1021
922
  });
923
+ child.on('error', (err) => {
924
+ clearTimeout(timeout);
925
+ job.status = 'failed';
926
+ job.endTime = Date.now();
927
+ console.error(`[ORCHESTRATOR] Worker ${id} error:`, err.message);
928
+ if (this.workerResultCallback) {
929
+ this.workerResultCallback(id, `Worker error: ${err.message}`);
930
+ }
931
+ });
932
+ // Return immediately - worker runs in background
933
+ return {
934
+ success: true,
935
+ output: `Worker ${id} started. I'll notify you when it completes.`
936
+ };
1022
937
  }
1023
938
  /**
1024
939
  * 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.21",
4
4
  "description": "Master orchestrator agent - control Claude Code remotely via web",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",