@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 +14 -6
- package/dist/core/hub.d.ts +45 -0
- package/dist/core/hub.js +231 -0
- package/dist/heartbeat.js +1 -1
- package/dist/orchestrator.d.ts +7 -2
- package/dist/orchestrator.js +95 -180
- 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
|
}
|
|
@@ -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;
|
package/dist/core/hub.js
ADDED
|
@@ -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.
|
|
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
|
@@ -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
|
|
833
|
-
*
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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';
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
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
|