@siftd/connect-agent 0.2.8 → 0.2.10
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/core/file-watcher.d.ts +109 -0
- package/dist/core/file-watcher.js +275 -0
- package/dist/heartbeat.js +1 -1
- package/dist/orchestrator.d.ts +28 -2
- package/dist/orchestrator.js +272 -10
- package/dist/workers/shared-state.d.ts +90 -0
- package/dist/workers/shared-state.js +254 -0
- package/package.json +1 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Watcher for Reactive Workflows
|
|
3
|
+
*
|
|
4
|
+
* Enables the orchestrator to:
|
|
5
|
+
* - Watch directories/files for changes
|
|
6
|
+
* - Trigger callbacks on file events
|
|
7
|
+
* - Support glob patterns for filtering
|
|
8
|
+
* - Debounce rapid changes
|
|
9
|
+
*/
|
|
10
|
+
import { EventEmitter } from 'events';
|
|
11
|
+
export type FileEventType = 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir';
|
|
12
|
+
export interface FileEvent {
|
|
13
|
+
type: FileEventType;
|
|
14
|
+
path: string;
|
|
15
|
+
timestamp: string;
|
|
16
|
+
}
|
|
17
|
+
export interface WatchRule {
|
|
18
|
+
id: string;
|
|
19
|
+
pattern: string;
|
|
20
|
+
events: FileEventType[];
|
|
21
|
+
action: string;
|
|
22
|
+
debounceMs?: number;
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
lastTriggered?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface WatcherConfig {
|
|
27
|
+
rulesFile: string;
|
|
28
|
+
defaultDebounce: number;
|
|
29
|
+
}
|
|
30
|
+
export declare class FileWatcher extends EventEmitter {
|
|
31
|
+
private watchers;
|
|
32
|
+
private rules;
|
|
33
|
+
private debounceTimers;
|
|
34
|
+
private config;
|
|
35
|
+
private rulesFile;
|
|
36
|
+
constructor(workspaceDir: string, config?: Partial<WatcherConfig>);
|
|
37
|
+
/**
|
|
38
|
+
* Add a watch rule
|
|
39
|
+
*/
|
|
40
|
+
addRule(rule: Omit<WatchRule, 'id' | 'enabled'>): string;
|
|
41
|
+
/**
|
|
42
|
+
* Remove a watch rule
|
|
43
|
+
*/
|
|
44
|
+
removeRule(id: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Enable/disable a rule
|
|
47
|
+
*/
|
|
48
|
+
toggleRule(id: string, enabled: boolean): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* List all rules
|
|
51
|
+
*/
|
|
52
|
+
listRules(): WatchRule[];
|
|
53
|
+
/**
|
|
54
|
+
* Get a specific rule
|
|
55
|
+
*/
|
|
56
|
+
getRule(id: string): WatchRule | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Watch a specific path
|
|
59
|
+
*/
|
|
60
|
+
watch(targetPath: string, callback: (event: FileEvent) => void): string;
|
|
61
|
+
/**
|
|
62
|
+
* Stop watching a specific watcher
|
|
63
|
+
*/
|
|
64
|
+
unwatch(watcherId: string): void;
|
|
65
|
+
/**
|
|
66
|
+
* Stop all watchers
|
|
67
|
+
*/
|
|
68
|
+
stopAll(): void;
|
|
69
|
+
/**
|
|
70
|
+
* Get watcher status
|
|
71
|
+
*/
|
|
72
|
+
status(): {
|
|
73
|
+
activeWatchers: number;
|
|
74
|
+
rules: number;
|
|
75
|
+
enabledRules: number;
|
|
76
|
+
};
|
|
77
|
+
private watchDirectory;
|
|
78
|
+
private watchFile;
|
|
79
|
+
private determineEventType;
|
|
80
|
+
private startWatching;
|
|
81
|
+
private stopWatching;
|
|
82
|
+
private loadRules;
|
|
83
|
+
private saveRules;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Common watch patterns for development workflows
|
|
87
|
+
*/
|
|
88
|
+
export declare const COMMON_PATTERNS: {
|
|
89
|
+
typescript: {
|
|
90
|
+
pattern: string;
|
|
91
|
+
events: FileEventType[];
|
|
92
|
+
action: string;
|
|
93
|
+
};
|
|
94
|
+
tests: {
|
|
95
|
+
pattern: string;
|
|
96
|
+
events: FileEventType[];
|
|
97
|
+
action: string;
|
|
98
|
+
};
|
|
99
|
+
config: {
|
|
100
|
+
pattern: string;
|
|
101
|
+
events: FileEventType[];
|
|
102
|
+
action: string;
|
|
103
|
+
};
|
|
104
|
+
markdown: {
|
|
105
|
+
pattern: string;
|
|
106
|
+
events: FileEventType[];
|
|
107
|
+
action: string;
|
|
108
|
+
};
|
|
109
|
+
};
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Watcher for Reactive Workflows
|
|
3
|
+
*
|
|
4
|
+
* Enables the orchestrator to:
|
|
5
|
+
* - Watch directories/files for changes
|
|
6
|
+
* - Trigger callbacks on file events
|
|
7
|
+
* - Support glob patterns for filtering
|
|
8
|
+
* - Debounce rapid changes
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { EventEmitter } from 'events';
|
|
13
|
+
export class FileWatcher extends EventEmitter {
|
|
14
|
+
watchers = new Map();
|
|
15
|
+
rules = new Map();
|
|
16
|
+
debounceTimers = new Map();
|
|
17
|
+
config;
|
|
18
|
+
rulesFile;
|
|
19
|
+
constructor(workspaceDir, config) {
|
|
20
|
+
super();
|
|
21
|
+
this.rulesFile = config?.rulesFile || path.join(workspaceDir, '.watch-rules.json');
|
|
22
|
+
this.config = {
|
|
23
|
+
rulesFile: this.rulesFile,
|
|
24
|
+
defaultDebounce: config?.defaultDebounce || 500
|
|
25
|
+
};
|
|
26
|
+
this.loadRules();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Add a watch rule
|
|
30
|
+
*/
|
|
31
|
+
addRule(rule) {
|
|
32
|
+
const id = `watch_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
33
|
+
const fullRule = {
|
|
34
|
+
...rule,
|
|
35
|
+
id,
|
|
36
|
+
enabled: true,
|
|
37
|
+
debounceMs: rule.debounceMs || this.config.defaultDebounce
|
|
38
|
+
};
|
|
39
|
+
this.rules.set(id, fullRule);
|
|
40
|
+
this.saveRules();
|
|
41
|
+
this.startWatching(fullRule);
|
|
42
|
+
return id;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Remove a watch rule
|
|
46
|
+
*/
|
|
47
|
+
removeRule(id) {
|
|
48
|
+
const rule = this.rules.get(id);
|
|
49
|
+
if (!rule)
|
|
50
|
+
return false;
|
|
51
|
+
this.stopWatching(rule);
|
|
52
|
+
this.rules.delete(id);
|
|
53
|
+
this.saveRules();
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Enable/disable a rule
|
|
58
|
+
*/
|
|
59
|
+
toggleRule(id, enabled) {
|
|
60
|
+
const rule = this.rules.get(id);
|
|
61
|
+
if (!rule)
|
|
62
|
+
return false;
|
|
63
|
+
rule.enabled = enabled;
|
|
64
|
+
if (enabled) {
|
|
65
|
+
this.startWatching(rule);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
this.stopWatching(rule);
|
|
69
|
+
}
|
|
70
|
+
this.saveRules();
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* List all rules
|
|
75
|
+
*/
|
|
76
|
+
listRules() {
|
|
77
|
+
return Array.from(this.rules.values());
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get a specific rule
|
|
81
|
+
*/
|
|
82
|
+
getRule(id) {
|
|
83
|
+
return this.rules.get(id);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Watch a specific path
|
|
87
|
+
*/
|
|
88
|
+
watch(targetPath, callback) {
|
|
89
|
+
const id = `direct_${Date.now()}`;
|
|
90
|
+
try {
|
|
91
|
+
const resolvedPath = path.resolve(targetPath);
|
|
92
|
+
const stat = fs.statSync(resolvedPath);
|
|
93
|
+
if (stat.isDirectory()) {
|
|
94
|
+
this.watchDirectory(resolvedPath, callback, id);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
this.watchFile(resolvedPath, callback, id);
|
|
98
|
+
}
|
|
99
|
+
return id;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
console.error(`[WATCHER] Failed to watch ${targetPath}:`, error);
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Stop watching a specific watcher
|
|
108
|
+
*/
|
|
109
|
+
unwatch(watcherId) {
|
|
110
|
+
const watcher = this.watchers.get(watcherId);
|
|
111
|
+
if (watcher) {
|
|
112
|
+
watcher.close();
|
|
113
|
+
this.watchers.delete(watcherId);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Stop all watchers
|
|
118
|
+
*/
|
|
119
|
+
stopAll() {
|
|
120
|
+
for (const [id, watcher] of this.watchers) {
|
|
121
|
+
watcher.close();
|
|
122
|
+
}
|
|
123
|
+
this.watchers.clear();
|
|
124
|
+
for (const timer of this.debounceTimers.values()) {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
}
|
|
127
|
+
this.debounceTimers.clear();
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Get watcher status
|
|
131
|
+
*/
|
|
132
|
+
status() {
|
|
133
|
+
return {
|
|
134
|
+
activeWatchers: this.watchers.size,
|
|
135
|
+
rules: this.rules.size,
|
|
136
|
+
enabledRules: Array.from(this.rules.values()).filter(r => r.enabled).length
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
watchDirectory(dirPath, callback, id) {
|
|
140
|
+
const watcher = fs.watch(dirPath, { recursive: true }, (eventType, filename) => {
|
|
141
|
+
if (!filename)
|
|
142
|
+
return;
|
|
143
|
+
const fullPath = path.join(dirPath, filename);
|
|
144
|
+
const event = {
|
|
145
|
+
type: eventType === 'rename' ? this.determineEventType(fullPath) : 'change',
|
|
146
|
+
path: fullPath,
|
|
147
|
+
timestamp: new Date().toISOString()
|
|
148
|
+
};
|
|
149
|
+
callback(event);
|
|
150
|
+
});
|
|
151
|
+
this.watchers.set(id, watcher);
|
|
152
|
+
}
|
|
153
|
+
watchFile(filePath, callback, id) {
|
|
154
|
+
const watcher = fs.watch(filePath, (eventType) => {
|
|
155
|
+
const event = {
|
|
156
|
+
type: eventType === 'rename' ? 'unlink' : 'change',
|
|
157
|
+
path: filePath,
|
|
158
|
+
timestamp: new Date().toISOString()
|
|
159
|
+
};
|
|
160
|
+
callback(event);
|
|
161
|
+
});
|
|
162
|
+
this.watchers.set(id, watcher);
|
|
163
|
+
}
|
|
164
|
+
determineEventType(filePath) {
|
|
165
|
+
try {
|
|
166
|
+
const stat = fs.statSync(filePath);
|
|
167
|
+
return stat.isDirectory() ? 'addDir' : 'add';
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return 'unlink';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
startWatching(rule) {
|
|
174
|
+
if (!rule.enabled)
|
|
175
|
+
return;
|
|
176
|
+
try {
|
|
177
|
+
const targetPath = path.resolve(rule.pattern);
|
|
178
|
+
// Check if path exists
|
|
179
|
+
if (!fs.existsSync(targetPath)) {
|
|
180
|
+
console.log(`[WATCHER] Path does not exist yet: ${targetPath}`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const callback = (event) => {
|
|
184
|
+
if (!rule.events.includes(event.type))
|
|
185
|
+
return;
|
|
186
|
+
// Debounce
|
|
187
|
+
const debounceKey = `${rule.id}_${event.path}`;
|
|
188
|
+
const existing = this.debounceTimers.get(debounceKey);
|
|
189
|
+
if (existing) {
|
|
190
|
+
clearTimeout(existing);
|
|
191
|
+
}
|
|
192
|
+
const timer = setTimeout(() => {
|
|
193
|
+
this.debounceTimers.delete(debounceKey);
|
|
194
|
+
rule.lastTriggered = new Date().toISOString();
|
|
195
|
+
this.saveRules();
|
|
196
|
+
// Emit event for orchestrator to handle
|
|
197
|
+
this.emit('trigger', {
|
|
198
|
+
rule,
|
|
199
|
+
event,
|
|
200
|
+
action: rule.action
|
|
201
|
+
});
|
|
202
|
+
}, rule.debounceMs || this.config.defaultDebounce);
|
|
203
|
+
this.debounceTimers.set(debounceKey, timer);
|
|
204
|
+
};
|
|
205
|
+
this.watch(targetPath, callback);
|
|
206
|
+
console.log(`[WATCHER] Started watching: ${rule.pattern}`);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
console.error(`[WATCHER] Failed to start watching ${rule.pattern}:`, error);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
stopWatching(rule) {
|
|
213
|
+
// Find and close watchers for this rule
|
|
214
|
+
for (const [id, watcher] of this.watchers) {
|
|
215
|
+
if (id.includes(rule.id)) {
|
|
216
|
+
watcher.close();
|
|
217
|
+
this.watchers.delete(id);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
loadRules() {
|
|
222
|
+
try {
|
|
223
|
+
if (fs.existsSync(this.rulesFile)) {
|
|
224
|
+
const data = JSON.parse(fs.readFileSync(this.rulesFile, 'utf8'));
|
|
225
|
+
for (const rule of data.rules || []) {
|
|
226
|
+
this.rules.set(rule.id, rule);
|
|
227
|
+
if (rule.enabled) {
|
|
228
|
+
this.startWatching(rule);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
console.log(`[WATCHER] Loaded ${this.rules.size} watch rules`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
console.error('[WATCHER] Failed to load rules:', error);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
saveRules() {
|
|
239
|
+
try {
|
|
240
|
+
const data = {
|
|
241
|
+
rules: Array.from(this.rules.values()),
|
|
242
|
+
savedAt: new Date().toISOString()
|
|
243
|
+
};
|
|
244
|
+
fs.writeFileSync(this.rulesFile, JSON.stringify(data, null, 2));
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
console.error('[WATCHER] Failed to save rules:', error);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Common watch patterns for development workflows
|
|
253
|
+
*/
|
|
254
|
+
export const COMMON_PATTERNS = {
|
|
255
|
+
typescript: {
|
|
256
|
+
pattern: '**/*.ts',
|
|
257
|
+
events: ['change', 'add'],
|
|
258
|
+
action: 'TypeScript file changed - run type check and tests'
|
|
259
|
+
},
|
|
260
|
+
tests: {
|
|
261
|
+
pattern: '**/*.test.ts',
|
|
262
|
+
events: ['change', 'add'],
|
|
263
|
+
action: 'Test file changed - run tests'
|
|
264
|
+
},
|
|
265
|
+
config: {
|
|
266
|
+
pattern: '**/package.json',
|
|
267
|
+
events: ['change'],
|
|
268
|
+
action: 'Package.json changed - check for dependency updates'
|
|
269
|
+
},
|
|
270
|
+
markdown: {
|
|
271
|
+
pattern: '**/*.md',
|
|
272
|
+
events: ['change', 'add'],
|
|
273
|
+
action: 'Documentation changed - validate links and formatting'
|
|
274
|
+
}
|
|
275
|
+
};
|
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.10'; // Should match package.json
|
|
14
14
|
const state = {
|
|
15
15
|
intervalId: null,
|
|
16
16
|
runnerId: null,
|
package/dist/orchestrator.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export declare class MasterOrchestrator {
|
|
|
22
22
|
private bashTool;
|
|
23
23
|
private webTools;
|
|
24
24
|
private workerTools;
|
|
25
|
+
private sharedState;
|
|
26
|
+
private fileWatcher;
|
|
25
27
|
private verboseMode;
|
|
26
28
|
constructor(options: {
|
|
27
29
|
apiKey: string;
|
|
@@ -78,8 +80,12 @@ export declare class MasterOrchestrator {
|
|
|
78
80
|
*/
|
|
79
81
|
private delegateToWorker;
|
|
80
82
|
/**
|
|
81
|
-
* Extract memory contributions from worker output
|
|
82
|
-
* Workers can contribute
|
|
83
|
+
* Extract memory and coordination contributions from worker output
|
|
84
|
+
* Workers can contribute using:
|
|
85
|
+
* - [MEMORY] type=X | content=Y
|
|
86
|
+
* - [SHARE] key=X | value=Y
|
|
87
|
+
* - [SIGNAL] name=X | data=Y
|
|
88
|
+
* - [MESSAGE] to=X | content=Y
|
|
83
89
|
*/
|
|
84
90
|
private extractWorkerMemories;
|
|
85
91
|
/**
|
|
@@ -114,6 +120,26 @@ export declare class MasterOrchestrator {
|
|
|
114
120
|
* Stop a running server on a port
|
|
115
121
|
*/
|
|
116
122
|
private executeStopServer;
|
|
123
|
+
/**
|
|
124
|
+
* Add a file watch rule
|
|
125
|
+
*/
|
|
126
|
+
private executeAddWatchRule;
|
|
127
|
+
/**
|
|
128
|
+
* Remove a file watch rule
|
|
129
|
+
*/
|
|
130
|
+
private executeRemoveWatchRule;
|
|
131
|
+
/**
|
|
132
|
+
* List all file watch rules
|
|
133
|
+
*/
|
|
134
|
+
private executeListWatchRules;
|
|
135
|
+
/**
|
|
136
|
+
* Toggle a file watch rule
|
|
137
|
+
*/
|
|
138
|
+
private executeToggleWatchRule;
|
|
139
|
+
/**
|
|
140
|
+
* Get file watcher status
|
|
141
|
+
*/
|
|
142
|
+
private executeWatchStatus;
|
|
117
143
|
/**
|
|
118
144
|
* Format tool preview for user
|
|
119
145
|
*/
|
package/dist/orchestrator.js
CHANGED
|
@@ -10,9 +10,11 @@ import { existsSync } from 'fs';
|
|
|
10
10
|
import { AdvancedMemoryStore } from './core/memory-advanced.js';
|
|
11
11
|
import { TaskScheduler } from './core/scheduler.js';
|
|
12
12
|
import { SystemIndexer } from './core/system-indexer.js';
|
|
13
|
+
import { FileWatcher } from './core/file-watcher.js';
|
|
13
14
|
import { BashTool } from './tools/bash.js';
|
|
14
15
|
import { WebTools } from './tools/web.js';
|
|
15
16
|
import { WorkerTools } from './tools/worker.js';
|
|
17
|
+
import { SharedState } from './workers/shared-state.js';
|
|
16
18
|
import { getKnowledgeForPrompt } from './genesis/index.js';
|
|
17
19
|
const SYSTEM_PROMPT = `You're the user's personal assistant - the friendly brain behind their Connect app. You chat with them from any browser, remember everything about them, and dispatch Claude Code workers to do things on their machine.
|
|
18
20
|
|
|
@@ -85,6 +87,8 @@ export class MasterOrchestrator {
|
|
|
85
87
|
bashTool;
|
|
86
88
|
webTools;
|
|
87
89
|
workerTools;
|
|
90
|
+
sharedState;
|
|
91
|
+
fileWatcher;
|
|
88
92
|
verboseMode = new Map(); // per-user verbose mode
|
|
89
93
|
constructor(options) {
|
|
90
94
|
this.client = new Anthropic({ apiKey: options.apiKey });
|
|
@@ -108,6 +112,15 @@ export class MasterOrchestrator {
|
|
|
108
112
|
this.bashTool = new BashTool(this.workspaceDir);
|
|
109
113
|
this.webTools = new WebTools();
|
|
110
114
|
this.workerTools = new WorkerTools(this.workspaceDir);
|
|
115
|
+
this.sharedState = new SharedState(this.workspaceDir);
|
|
116
|
+
this.fileWatcher = new FileWatcher(this.workspaceDir);
|
|
117
|
+
// Set up file watcher trigger handler
|
|
118
|
+
this.fileWatcher.on('trigger', async ({ rule, event, action }) => {
|
|
119
|
+
console.log(`[WATCHER] Triggered: ${rule.pattern} (${event.type}: ${event.path})`);
|
|
120
|
+
// Auto-delegate to worker when file changes
|
|
121
|
+
const task = `${action}\n\nTriggered by file change:\n- File: ${event.path}\n- Event: ${event.type}\n- Rule: ${rule.pattern}`;
|
|
122
|
+
this.delegateToWorker(task, undefined, this.workspaceDir);
|
|
123
|
+
});
|
|
111
124
|
}
|
|
112
125
|
/**
|
|
113
126
|
* Initialize the orchestrator - indexes filesystem on first call
|
|
@@ -590,6 +603,84 @@ Be specific about what you want done.`,
|
|
|
590
603
|
},
|
|
591
604
|
required: ['port']
|
|
592
605
|
}
|
|
606
|
+
},
|
|
607
|
+
// File watching tools for reactive workflows
|
|
608
|
+
{
|
|
609
|
+
name: 'add_watch_rule',
|
|
610
|
+
description: 'Add a file watch rule to automatically trigger actions when files change. Useful for reactive workflows like auto-running tests when code changes.',
|
|
611
|
+
input_schema: {
|
|
612
|
+
type: 'object',
|
|
613
|
+
properties: {
|
|
614
|
+
pattern: {
|
|
615
|
+
type: 'string',
|
|
616
|
+
description: 'File path or directory to watch (e.g., "./src", "/path/to/file.ts")'
|
|
617
|
+
},
|
|
618
|
+
events: {
|
|
619
|
+
type: 'array',
|
|
620
|
+
items: { type: 'string', enum: ['add', 'change', 'unlink', 'addDir', 'unlinkDir'] },
|
|
621
|
+
description: 'Events to watch for (default: ["change", "add"])'
|
|
622
|
+
},
|
|
623
|
+
action: {
|
|
624
|
+
type: 'string',
|
|
625
|
+
description: 'Task description to execute when triggered (sent to a worker)'
|
|
626
|
+
},
|
|
627
|
+
debounce_ms: {
|
|
628
|
+
type: 'number',
|
|
629
|
+
description: 'Debounce delay in milliseconds (default: 500)'
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
required: ['pattern', 'action']
|
|
633
|
+
}
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
name: 'remove_watch_rule',
|
|
637
|
+
description: 'Remove a file watch rule by its ID.',
|
|
638
|
+
input_schema: {
|
|
639
|
+
type: 'object',
|
|
640
|
+
properties: {
|
|
641
|
+
rule_id: {
|
|
642
|
+
type: 'string',
|
|
643
|
+
description: 'The ID of the watch rule to remove'
|
|
644
|
+
}
|
|
645
|
+
},
|
|
646
|
+
required: ['rule_id']
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
name: 'list_watch_rules',
|
|
651
|
+
description: 'List all active file watch rules.',
|
|
652
|
+
input_schema: {
|
|
653
|
+
type: 'object',
|
|
654
|
+
properties: {},
|
|
655
|
+
required: []
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
name: 'toggle_watch_rule',
|
|
660
|
+
description: 'Enable or disable a file watch rule.',
|
|
661
|
+
input_schema: {
|
|
662
|
+
type: 'object',
|
|
663
|
+
properties: {
|
|
664
|
+
rule_id: {
|
|
665
|
+
type: 'string',
|
|
666
|
+
description: 'The ID of the watch rule to toggle'
|
|
667
|
+
},
|
|
668
|
+
enabled: {
|
|
669
|
+
type: 'boolean',
|
|
670
|
+
description: 'Whether to enable (true) or disable (false) the rule'
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
required: ['rule_id', 'enabled']
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
name: 'watch_status',
|
|
678
|
+
description: 'Get the status of the file watcher system.',
|
|
679
|
+
input_schema: {
|
|
680
|
+
type: 'object',
|
|
681
|
+
properties: {},
|
|
682
|
+
required: []
|
|
683
|
+
}
|
|
593
684
|
}
|
|
594
685
|
];
|
|
595
686
|
}
|
|
@@ -669,6 +760,22 @@ Be specific about what you want done.`,
|
|
|
669
760
|
case 'stop_local_server':
|
|
670
761
|
result = await this.executeStopServer(input.port);
|
|
671
762
|
break;
|
|
763
|
+
// File watching tools
|
|
764
|
+
case 'add_watch_rule':
|
|
765
|
+
result = await this.executeAddWatchRule(input.pattern, input.action, input.events, input.debounce_ms);
|
|
766
|
+
break;
|
|
767
|
+
case 'remove_watch_rule':
|
|
768
|
+
result = await this.executeRemoveWatchRule(input.rule_id);
|
|
769
|
+
break;
|
|
770
|
+
case 'list_watch_rules':
|
|
771
|
+
result = await this.executeListWatchRules();
|
|
772
|
+
break;
|
|
773
|
+
case 'toggle_watch_rule':
|
|
774
|
+
result = await this.executeToggleWatchRule(input.rule_id, input.enabled);
|
|
775
|
+
break;
|
|
776
|
+
case 'watch_status':
|
|
777
|
+
result = await this.executeWatchStatus();
|
|
778
|
+
break;
|
|
672
779
|
default:
|
|
673
780
|
result = { success: false, output: `Unknown tool: ${toolUse.name}` };
|
|
674
781
|
}
|
|
@@ -703,6 +810,8 @@ Be specific about what you want done.`,
|
|
|
703
810
|
// Memory search failed, continue without it
|
|
704
811
|
console.log('[ORCHESTRATOR] Memory search failed, continuing without context');
|
|
705
812
|
}
|
|
813
|
+
// Get shared state context for worker coordination
|
|
814
|
+
const sharedContext = this.sharedState.getSummaryForWorker(id);
|
|
706
815
|
// Build prompt for worker with memory context and checkpoint instructions
|
|
707
816
|
let prompt = task;
|
|
708
817
|
if (context) {
|
|
@@ -711,6 +820,9 @@ Be specific about what you want done.`,
|
|
|
711
820
|
if (memoryContext) {
|
|
712
821
|
prompt += memoryContext;
|
|
713
822
|
}
|
|
823
|
+
if (sharedContext) {
|
|
824
|
+
prompt += `\n\nWORKER COORDINATION:${sharedContext}`;
|
|
825
|
+
}
|
|
714
826
|
// Add checkpoint and logging instructions to prevent data loss
|
|
715
827
|
const logFile = `/tmp/worker-${id}-log.txt`;
|
|
716
828
|
prompt += `
|
|
@@ -730,7 +842,14 @@ If you discover something important (patterns, user preferences, technical insig
|
|
|
730
842
|
end your response with a line like:
|
|
731
843
|
[MEMORY] type=semantic | content=User prefers X over Y for this type of task
|
|
732
844
|
[MEMORY] type=procedural | content=When doing X, always check Y first
|
|
733
|
-
This helps me remember and improve for future tasks
|
|
845
|
+
This helps me remember and improve for future tasks.
|
|
846
|
+
|
|
847
|
+
WORKER COORDINATION (optional):
|
|
848
|
+
If you need to share data with other workers or signal completion:
|
|
849
|
+
[SHARE] key=myData | value={"result": "something useful"}
|
|
850
|
+
[SIGNAL] name=step1_complete | data={"files": ["a.ts", "b.ts"]}
|
|
851
|
+
[MESSAGE] to=worker_xyz | content=Please review the changes I made
|
|
852
|
+
This enables parallel workers to coordinate.`;
|
|
734
853
|
console.log(`[ORCHESTRATOR] Delegating to worker ${id}: ${task.slice(0, 80)}...`);
|
|
735
854
|
return new Promise((resolve) => {
|
|
736
855
|
const job = {
|
|
@@ -822,37 +941,76 @@ This helps me remember and improve for future tasks.`;
|
|
|
822
941
|
});
|
|
823
942
|
}
|
|
824
943
|
/**
|
|
825
|
-
* Extract memory contributions from worker output
|
|
826
|
-
* Workers can contribute
|
|
944
|
+
* Extract memory and coordination contributions from worker output
|
|
945
|
+
* Workers can contribute using:
|
|
946
|
+
* - [MEMORY] type=X | content=Y
|
|
947
|
+
* - [SHARE] key=X | value=Y
|
|
948
|
+
* - [SIGNAL] name=X | data=Y
|
|
949
|
+
* - [MESSAGE] to=X | content=Y
|
|
827
950
|
*/
|
|
828
951
|
async extractWorkerMemories(output, workerId) {
|
|
829
|
-
|
|
952
|
+
let memoryCount = 0;
|
|
953
|
+
let coordCount = 0;
|
|
954
|
+
// Extract memory contributions
|
|
830
955
|
const memoryPattern = /\[MEMORY\]\s*type=(\w+)\s*\|\s*content=(.+?)(?=\n|$)/g;
|
|
831
956
|
let match;
|
|
832
|
-
let count = 0;
|
|
833
957
|
while ((match = memoryPattern.exec(output)) !== null) {
|
|
834
958
|
const type = match[1].toLowerCase();
|
|
835
959
|
const content = match[2].trim();
|
|
836
960
|
if (!content)
|
|
837
961
|
continue;
|
|
838
|
-
// Validate memory type
|
|
839
962
|
const validTypes = ['episodic', 'semantic', 'procedural'];
|
|
840
963
|
const memType = validTypes.includes(type) ? type : 'semantic';
|
|
841
964
|
try {
|
|
842
965
|
await this.memory.remember(content, {
|
|
843
966
|
type: memType,
|
|
844
967
|
source: `worker:${workerId}`,
|
|
845
|
-
importance: 0.7,
|
|
968
|
+
importance: 0.7,
|
|
846
969
|
tags: ['worker-contributed', 'auto-learned']
|
|
847
970
|
});
|
|
848
|
-
|
|
971
|
+
memoryCount++;
|
|
849
972
|
}
|
|
850
973
|
catch (error) {
|
|
851
974
|
console.log(`[ORCHESTRATOR] Failed to store worker memory: ${error}`);
|
|
852
975
|
}
|
|
853
976
|
}
|
|
854
|
-
|
|
855
|
-
|
|
977
|
+
// Extract shared state contributions
|
|
978
|
+
const sharePattern = /\[SHARE\]\s*key=(\w+)\s*\|\s*value=(.+?)(?=\n|$)/g;
|
|
979
|
+
while ((match = sharePattern.exec(output)) !== null) {
|
|
980
|
+
const key = match[1];
|
|
981
|
+
let value = match[2].trim();
|
|
982
|
+
try {
|
|
983
|
+
value = JSON.parse(value);
|
|
984
|
+
}
|
|
985
|
+
catch { /* use as string */ }
|
|
986
|
+
this.sharedState.set(key, value, workerId);
|
|
987
|
+
coordCount++;
|
|
988
|
+
}
|
|
989
|
+
// Extract signal completions
|
|
990
|
+
const signalPattern = /\[SIGNAL\]\s*name=(\w+)(?:\s*\|\s*data=(.+?))?(?=\n|$)/g;
|
|
991
|
+
while ((match = signalPattern.exec(output)) !== null) {
|
|
992
|
+
const name = match[1];
|
|
993
|
+
let data = match[2]?.trim();
|
|
994
|
+
try {
|
|
995
|
+
data = data ? JSON.parse(data) : undefined;
|
|
996
|
+
}
|
|
997
|
+
catch { /* use as string */ }
|
|
998
|
+
this.sharedState.signalComplete(name, workerId, data);
|
|
999
|
+
coordCount++;
|
|
1000
|
+
}
|
|
1001
|
+
// Extract messages
|
|
1002
|
+
const messagePattern = /\[MESSAGE\]\s*to=(\w+)\s*\|\s*content=(.+?)(?=\n|$)/g;
|
|
1003
|
+
while ((match = messagePattern.exec(output)) !== null) {
|
|
1004
|
+
const to = match[1];
|
|
1005
|
+
const content = match[2].trim();
|
|
1006
|
+
this.sharedState.sendMessage(workerId, to, 'data', content);
|
|
1007
|
+
coordCount++;
|
|
1008
|
+
}
|
|
1009
|
+
if (memoryCount > 0) {
|
|
1010
|
+
console.log(`[ORCHESTRATOR] Extracted ${memoryCount} memory contributions from worker ${workerId}`);
|
|
1011
|
+
}
|
|
1012
|
+
if (coordCount > 0) {
|
|
1013
|
+
console.log(`[ORCHESTRATOR] Extracted ${coordCount} coordination messages from worker ${workerId}`);
|
|
856
1014
|
}
|
|
857
1015
|
}
|
|
858
1016
|
/**
|
|
@@ -1014,6 +1172,99 @@ This helps me remember and improve for future tasks.`;
|
|
|
1014
1172
|
return { success: false, output: `No server running on port ${port}` };
|
|
1015
1173
|
}
|
|
1016
1174
|
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Add a file watch rule
|
|
1177
|
+
*/
|
|
1178
|
+
async executeAddWatchRule(pattern, action, events, debounceMs) {
|
|
1179
|
+
try {
|
|
1180
|
+
const validEvents = ['add', 'change', 'unlink', 'addDir', 'unlinkDir'];
|
|
1181
|
+
const eventList = events
|
|
1182
|
+
? events.filter((e) => validEvents.includes(e))
|
|
1183
|
+
: ['change', 'add'];
|
|
1184
|
+
const ruleId = this.fileWatcher.addRule({
|
|
1185
|
+
pattern,
|
|
1186
|
+
events: eventList,
|
|
1187
|
+
action,
|
|
1188
|
+
debounceMs
|
|
1189
|
+
});
|
|
1190
|
+
console.log(`[ORCHESTRATOR] Added watch rule ${ruleId}: ${pattern}`);
|
|
1191
|
+
return {
|
|
1192
|
+
success: true,
|
|
1193
|
+
output: `Watch rule added:\n- ID: ${ruleId}\n- Pattern: ${pattern}\n- Events: ${eventList.join(', ')}\n- Action: ${action}\n- Debounce: ${debounceMs || 500}ms`
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
catch (error) {
|
|
1197
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1198
|
+
return { success: false, output: `Failed to add watch rule: ${msg}` };
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Remove a file watch rule
|
|
1203
|
+
*/
|
|
1204
|
+
async executeRemoveWatchRule(ruleId) {
|
|
1205
|
+
try {
|
|
1206
|
+
const removed = this.fileWatcher.removeRule(ruleId);
|
|
1207
|
+
if (removed) {
|
|
1208
|
+
console.log(`[ORCHESTRATOR] Removed watch rule ${ruleId}`);
|
|
1209
|
+
return { success: true, output: `Watch rule ${ruleId} removed` };
|
|
1210
|
+
}
|
|
1211
|
+
return { success: false, output: `Watch rule ${ruleId} not found` };
|
|
1212
|
+
}
|
|
1213
|
+
catch (error) {
|
|
1214
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1215
|
+
return { success: false, output: `Failed to remove watch rule: ${msg}` };
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* List all file watch rules
|
|
1220
|
+
*/
|
|
1221
|
+
async executeListWatchRules() {
|
|
1222
|
+
try {
|
|
1223
|
+
const rules = this.fileWatcher.listRules();
|
|
1224
|
+
if (rules.length === 0) {
|
|
1225
|
+
return { success: true, output: 'No watch rules configured.' };
|
|
1226
|
+
}
|
|
1227
|
+
const output = rules.map(r => `${r.enabled ? '✓' : '⏸'} ${r.id}\n Pattern: ${r.pattern}\n Events: ${r.events.join(', ')}\n Action: ${r.action.slice(0, 60)}...${r.lastTriggered ? `\n Last triggered: ${r.lastTriggered}` : ''}`).join('\n\n');
|
|
1228
|
+
return { success: true, output: `Watch Rules (${rules.length}):\n\n${output}` };
|
|
1229
|
+
}
|
|
1230
|
+
catch (error) {
|
|
1231
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1232
|
+
return { success: false, output: `Failed to list watch rules: ${msg}` };
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Toggle a file watch rule
|
|
1237
|
+
*/
|
|
1238
|
+
async executeToggleWatchRule(ruleId, enabled) {
|
|
1239
|
+
try {
|
|
1240
|
+
const toggled = this.fileWatcher.toggleRule(ruleId, enabled);
|
|
1241
|
+
if (toggled) {
|
|
1242
|
+
console.log(`[ORCHESTRATOR] ${enabled ? 'Enabled' : 'Disabled'} watch rule ${ruleId}`);
|
|
1243
|
+
return { success: true, output: `Watch rule ${ruleId} ${enabled ? 'enabled' : 'disabled'}` };
|
|
1244
|
+
}
|
|
1245
|
+
return { success: false, output: `Watch rule ${ruleId} not found` };
|
|
1246
|
+
}
|
|
1247
|
+
catch (error) {
|
|
1248
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1249
|
+
return { success: false, output: `Failed to toggle watch rule: ${msg}` };
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Get file watcher status
|
|
1254
|
+
*/
|
|
1255
|
+
async executeWatchStatus() {
|
|
1256
|
+
try {
|
|
1257
|
+
const status = this.fileWatcher.status();
|
|
1258
|
+
return {
|
|
1259
|
+
success: true,
|
|
1260
|
+
output: `File Watcher Status:\n- Active watchers: ${status.activeWatchers}\n- Total rules: ${status.rules}\n- Enabled rules: ${status.enabledRules}`
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
catch (error) {
|
|
1264
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1265
|
+
return { success: false, output: `Failed to get watcher status: ${msg}` };
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1017
1268
|
/**
|
|
1018
1269
|
* Format tool preview for user
|
|
1019
1270
|
*/
|
|
@@ -1057,6 +1308,16 @@ This helps me remember and improve for future tasks.`;
|
|
|
1057
1308
|
return `Starting server on port ${input.port || 8080}...`;
|
|
1058
1309
|
case 'stop_local_server':
|
|
1059
1310
|
return `Stopping server on port ${input.port}...`;
|
|
1311
|
+
case 'add_watch_rule':
|
|
1312
|
+
return `Adding watch rule for ${input.pattern}...`;
|
|
1313
|
+
case 'remove_watch_rule':
|
|
1314
|
+
return `Removing watch rule ${input.rule_id}...`;
|
|
1315
|
+
case 'list_watch_rules':
|
|
1316
|
+
return 'Listing watch rules...';
|
|
1317
|
+
case 'toggle_watch_rule':
|
|
1318
|
+
return `${input.enabled ? 'Enabling' : 'Disabling'} watch rule ${input.rule_id}...`;
|
|
1319
|
+
case 'watch_status':
|
|
1320
|
+
return 'Getting watcher status...';
|
|
1060
1321
|
default:
|
|
1061
1322
|
return null;
|
|
1062
1323
|
}
|
|
@@ -1077,6 +1338,7 @@ This helps me remember and improve for future tasks.`;
|
|
|
1077
1338
|
* Shutdown cleanly
|
|
1078
1339
|
*/
|
|
1079
1340
|
shutdown() {
|
|
1341
|
+
this.fileWatcher.stopAll();
|
|
1080
1342
|
this.scheduler.shutdown();
|
|
1081
1343
|
this.memory.close();
|
|
1082
1344
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared State for Worker Communication
|
|
3
|
+
*
|
|
4
|
+
* Enables workers to:
|
|
5
|
+
* - Share data via a common state store
|
|
6
|
+
* - Signal completion of dependencies
|
|
7
|
+
* - Lock resources to prevent conflicts
|
|
8
|
+
* - Pass messages between workers
|
|
9
|
+
*/
|
|
10
|
+
export interface SharedMessage {
|
|
11
|
+
from: string;
|
|
12
|
+
to: string | '*';
|
|
13
|
+
type: 'data' | 'signal' | 'request';
|
|
14
|
+
content: string;
|
|
15
|
+
timestamp: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ResourceLock {
|
|
18
|
+
resource: string;
|
|
19
|
+
owner: string;
|
|
20
|
+
acquired: string;
|
|
21
|
+
expires: string;
|
|
22
|
+
}
|
|
23
|
+
export declare class SharedState {
|
|
24
|
+
private stateDir;
|
|
25
|
+
private stateFile;
|
|
26
|
+
private messagesFile;
|
|
27
|
+
private locksFile;
|
|
28
|
+
constructor(workspaceDir: string);
|
|
29
|
+
private ensureStateDir;
|
|
30
|
+
/**
|
|
31
|
+
* Set a shared value
|
|
32
|
+
*/
|
|
33
|
+
set(key: string, value: unknown, workerId?: string): void;
|
|
34
|
+
/**
|
|
35
|
+
* Get a shared value
|
|
36
|
+
*/
|
|
37
|
+
get(key: string): unknown;
|
|
38
|
+
/**
|
|
39
|
+
* Get all shared state
|
|
40
|
+
*/
|
|
41
|
+
getAll(): Record<string, unknown>;
|
|
42
|
+
/**
|
|
43
|
+
* Signal that a worker has completed a specific task/dependency
|
|
44
|
+
*/
|
|
45
|
+
signalComplete(signalName: string, workerId: string, data?: unknown): void;
|
|
46
|
+
/**
|
|
47
|
+
* Wait for a signal (polling-based)
|
|
48
|
+
* Returns true if signal is already set, false if not
|
|
49
|
+
*/
|
|
50
|
+
checkSignal(signalName: string): {
|
|
51
|
+
completed: boolean;
|
|
52
|
+
data?: unknown;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Send a message to another worker
|
|
56
|
+
*/
|
|
57
|
+
sendMessage(from: string, to: string, type: 'data' | 'signal' | 'request', content: string): void;
|
|
58
|
+
/**
|
|
59
|
+
* Get messages for a worker
|
|
60
|
+
*/
|
|
61
|
+
getMessages(workerId: string, since?: string): SharedMessage[];
|
|
62
|
+
/**
|
|
63
|
+
* Try to acquire a resource lock
|
|
64
|
+
*/
|
|
65
|
+
acquireLock(resource: string, workerId: string, durationMs?: number): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Release a resource lock
|
|
68
|
+
*/
|
|
69
|
+
releaseLock(resource: string, workerId: string): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Clean up expired locks
|
|
72
|
+
*/
|
|
73
|
+
cleanupExpiredLocks(): void;
|
|
74
|
+
/**
|
|
75
|
+
* Get summary for injecting into worker prompts
|
|
76
|
+
*/
|
|
77
|
+
getSummaryForWorker(workerId: string): string;
|
|
78
|
+
/**
|
|
79
|
+
* Clear all shared state (for cleanup between sessions)
|
|
80
|
+
*/
|
|
81
|
+
clear(): void;
|
|
82
|
+
private loadState;
|
|
83
|
+
private saveState;
|
|
84
|
+
private loadSignals;
|
|
85
|
+
private saveSignals;
|
|
86
|
+
private loadMessages;
|
|
87
|
+
private saveMessages;
|
|
88
|
+
private loadLocks;
|
|
89
|
+
private saveLocks;
|
|
90
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared State for Worker Communication
|
|
3
|
+
*
|
|
4
|
+
* Enables workers to:
|
|
5
|
+
* - Share data via a common state store
|
|
6
|
+
* - Signal completion of dependencies
|
|
7
|
+
* - Lock resources to prevent conflicts
|
|
8
|
+
* - Pass messages between workers
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
export class SharedState {
|
|
13
|
+
stateDir;
|
|
14
|
+
stateFile;
|
|
15
|
+
messagesFile;
|
|
16
|
+
locksFile;
|
|
17
|
+
constructor(workspaceDir) {
|
|
18
|
+
this.stateDir = path.join(workspaceDir, '.worker-state');
|
|
19
|
+
this.stateFile = path.join(this.stateDir, 'state.json');
|
|
20
|
+
this.messagesFile = path.join(this.stateDir, 'messages.json');
|
|
21
|
+
this.locksFile = path.join(this.stateDir, 'locks.json');
|
|
22
|
+
this.ensureStateDir();
|
|
23
|
+
}
|
|
24
|
+
ensureStateDir() {
|
|
25
|
+
if (!fs.existsSync(this.stateDir)) {
|
|
26
|
+
fs.mkdirSync(this.stateDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Set a shared value
|
|
31
|
+
*/
|
|
32
|
+
set(key, value, workerId) {
|
|
33
|
+
const state = this.loadState();
|
|
34
|
+
state[key] = {
|
|
35
|
+
value,
|
|
36
|
+
setBy: workerId || 'unknown',
|
|
37
|
+
timestamp: new Date().toISOString()
|
|
38
|
+
};
|
|
39
|
+
this.saveState(state);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get a shared value
|
|
43
|
+
*/
|
|
44
|
+
get(key) {
|
|
45
|
+
const state = this.loadState();
|
|
46
|
+
return state[key]?.value;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get all shared state
|
|
50
|
+
*/
|
|
51
|
+
getAll() {
|
|
52
|
+
const state = this.loadState();
|
|
53
|
+
const result = {};
|
|
54
|
+
for (const [key, entry] of Object.entries(state)) {
|
|
55
|
+
result[key] = entry.value;
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Signal that a worker has completed a specific task/dependency
|
|
61
|
+
*/
|
|
62
|
+
signalComplete(signalName, workerId, data) {
|
|
63
|
+
const signals = this.loadSignals();
|
|
64
|
+
signals[signalName] = {
|
|
65
|
+
completed: true,
|
|
66
|
+
completedBy: workerId,
|
|
67
|
+
timestamp: new Date().toISOString(),
|
|
68
|
+
data
|
|
69
|
+
};
|
|
70
|
+
this.saveSignals(signals);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Wait for a signal (polling-based)
|
|
74
|
+
* Returns true if signal is already set, false if not
|
|
75
|
+
*/
|
|
76
|
+
checkSignal(signalName) {
|
|
77
|
+
const signals = this.loadSignals();
|
|
78
|
+
const signal = signals[signalName];
|
|
79
|
+
if (signal?.completed) {
|
|
80
|
+
return { completed: true, data: signal.data };
|
|
81
|
+
}
|
|
82
|
+
return { completed: false };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Send a message to another worker
|
|
86
|
+
*/
|
|
87
|
+
sendMessage(from, to, type, content) {
|
|
88
|
+
const messages = this.loadMessages();
|
|
89
|
+
messages.push({
|
|
90
|
+
from,
|
|
91
|
+
to,
|
|
92
|
+
type,
|
|
93
|
+
content,
|
|
94
|
+
timestamp: new Date().toISOString()
|
|
95
|
+
});
|
|
96
|
+
// Keep last 100 messages
|
|
97
|
+
if (messages.length > 100) {
|
|
98
|
+
messages.splice(0, messages.length - 100);
|
|
99
|
+
}
|
|
100
|
+
this.saveMessages(messages);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Get messages for a worker
|
|
104
|
+
*/
|
|
105
|
+
getMessages(workerId, since) {
|
|
106
|
+
const messages = this.loadMessages();
|
|
107
|
+
return messages.filter(m => {
|
|
108
|
+
const isForMe = m.to === workerId || m.to === '*';
|
|
109
|
+
const isAfterSince = !since || m.timestamp > since;
|
|
110
|
+
return isForMe && isAfterSince;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Try to acquire a resource lock
|
|
115
|
+
*/
|
|
116
|
+
acquireLock(resource, workerId, durationMs = 60000) {
|
|
117
|
+
const locks = this.loadLocks();
|
|
118
|
+
const now = new Date();
|
|
119
|
+
const existing = locks[resource];
|
|
120
|
+
// Check if existing lock is still valid
|
|
121
|
+
if (existing && new Date(existing.expires) > now && existing.owner !== workerId) {
|
|
122
|
+
return false; // Lock held by another worker
|
|
123
|
+
}
|
|
124
|
+
// Acquire lock
|
|
125
|
+
locks[resource] = {
|
|
126
|
+
resource,
|
|
127
|
+
owner: workerId,
|
|
128
|
+
acquired: now.toISOString(),
|
|
129
|
+
expires: new Date(now.getTime() + durationMs).toISOString()
|
|
130
|
+
};
|
|
131
|
+
this.saveLocks(locks);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Release a resource lock
|
|
136
|
+
*/
|
|
137
|
+
releaseLock(resource, workerId) {
|
|
138
|
+
const locks = this.loadLocks();
|
|
139
|
+
if (locks[resource]?.owner === workerId) {
|
|
140
|
+
delete locks[resource];
|
|
141
|
+
this.saveLocks(locks);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Clean up expired locks
|
|
148
|
+
*/
|
|
149
|
+
cleanupExpiredLocks() {
|
|
150
|
+
const locks = this.loadLocks();
|
|
151
|
+
const now = new Date();
|
|
152
|
+
let changed = false;
|
|
153
|
+
for (const [resource, lock] of Object.entries(locks)) {
|
|
154
|
+
if (new Date(lock.expires) < now) {
|
|
155
|
+
delete locks[resource];
|
|
156
|
+
changed = true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (changed) {
|
|
160
|
+
this.saveLocks(locks);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Get summary for injecting into worker prompts
|
|
165
|
+
*/
|
|
166
|
+
getSummaryForWorker(workerId) {
|
|
167
|
+
const state = this.getAll();
|
|
168
|
+
const messages = this.getMessages(workerId);
|
|
169
|
+
const signals = this.loadSignals();
|
|
170
|
+
let summary = '';
|
|
171
|
+
if (Object.keys(state).length > 0) {
|
|
172
|
+
summary += '\n## Shared State:\n';
|
|
173
|
+
for (const [key, value] of Object.entries(state)) {
|
|
174
|
+
summary += `- ${key}: ${JSON.stringify(value)}\n`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (messages.length > 0) {
|
|
178
|
+
summary += '\n## Messages for you:\n';
|
|
179
|
+
for (const msg of messages.slice(-5)) { // Last 5 messages
|
|
180
|
+
summary += `- [${msg.from}] ${msg.content}\n`;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const completedSignals = Object.entries(signals).filter(([, s]) => s.completed);
|
|
184
|
+
if (completedSignals.length > 0) {
|
|
185
|
+
summary += '\n## Completed dependencies:\n';
|
|
186
|
+
for (const [name] of completedSignals) {
|
|
187
|
+
summary += `- ${name} ✓\n`;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return summary;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Clear all shared state (for cleanup between sessions)
|
|
194
|
+
*/
|
|
195
|
+
clear() {
|
|
196
|
+
if (fs.existsSync(this.stateFile))
|
|
197
|
+
fs.unlinkSync(this.stateFile);
|
|
198
|
+
if (fs.existsSync(this.messagesFile))
|
|
199
|
+
fs.unlinkSync(this.messagesFile);
|
|
200
|
+
if (fs.existsSync(this.locksFile))
|
|
201
|
+
fs.unlinkSync(this.locksFile);
|
|
202
|
+
}
|
|
203
|
+
// Private helpers
|
|
204
|
+
loadState() {
|
|
205
|
+
try {
|
|
206
|
+
if (fs.existsSync(this.stateFile)) {
|
|
207
|
+
return JSON.parse(fs.readFileSync(this.stateFile, 'utf8'));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch { /* ignore */ }
|
|
211
|
+
return {};
|
|
212
|
+
}
|
|
213
|
+
saveState(state) {
|
|
214
|
+
fs.writeFileSync(this.stateFile, JSON.stringify(state, null, 2));
|
|
215
|
+
}
|
|
216
|
+
loadSignals() {
|
|
217
|
+
try {
|
|
218
|
+
const signalsFile = path.join(this.stateDir, 'signals.json');
|
|
219
|
+
if (fs.existsSync(signalsFile)) {
|
|
220
|
+
return JSON.parse(fs.readFileSync(signalsFile, 'utf8'));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch { /* ignore */ }
|
|
224
|
+
return {};
|
|
225
|
+
}
|
|
226
|
+
saveSignals(signals) {
|
|
227
|
+
const signalsFile = path.join(this.stateDir, 'signals.json');
|
|
228
|
+
fs.writeFileSync(signalsFile, JSON.stringify(signals, null, 2));
|
|
229
|
+
}
|
|
230
|
+
loadMessages() {
|
|
231
|
+
try {
|
|
232
|
+
if (fs.existsSync(this.messagesFile)) {
|
|
233
|
+
return JSON.parse(fs.readFileSync(this.messagesFile, 'utf8'));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch { /* ignore */ }
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
saveMessages(messages) {
|
|
240
|
+
fs.writeFileSync(this.messagesFile, JSON.stringify(messages, null, 2));
|
|
241
|
+
}
|
|
242
|
+
loadLocks() {
|
|
243
|
+
try {
|
|
244
|
+
if (fs.existsSync(this.locksFile)) {
|
|
245
|
+
return JSON.parse(fs.readFileSync(this.locksFile, 'utf8'));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch { /* ignore */ }
|
|
249
|
+
return {};
|
|
250
|
+
}
|
|
251
|
+
saveLocks(locks) {
|
|
252
|
+
fs.writeFileSync(this.locksFile, JSON.stringify(locks, null, 2));
|
|
253
|
+
}
|
|
254
|
+
}
|