@siftd/connect-agent 0.2.7 → 0.2.9
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/heartbeat.js +1 -1
- package/dist/orchestrator.d.ts +10 -0
- package/dist/orchestrator.js +116 -3
- package/dist/workers/shared-state.d.ts +90 -0
- package/dist/workers/shared-state.js +254 -0
- package/package.json +1 -1
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.9'; // 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,7 @@ export declare class MasterOrchestrator {
|
|
|
22
22
|
private bashTool;
|
|
23
23
|
private webTools;
|
|
24
24
|
private workerTools;
|
|
25
|
+
private sharedState;
|
|
25
26
|
private verboseMode;
|
|
26
27
|
constructor(options: {
|
|
27
28
|
apiKey: string;
|
|
@@ -77,6 +78,15 @@ export declare class MasterOrchestrator {
|
|
|
77
78
|
* Delegate task to Claude Code CLI worker with retry logic
|
|
78
79
|
*/
|
|
79
80
|
private delegateToWorker;
|
|
81
|
+
/**
|
|
82
|
+
* Extract memory and coordination contributions from worker output
|
|
83
|
+
* Workers can contribute using:
|
|
84
|
+
* - [MEMORY] type=X | content=Y
|
|
85
|
+
* - [SHARE] key=X | value=Y
|
|
86
|
+
* - [SIGNAL] name=X | data=Y
|
|
87
|
+
* - [MESSAGE] to=X | content=Y
|
|
88
|
+
*/
|
|
89
|
+
private extractWorkerMemories;
|
|
80
90
|
/**
|
|
81
91
|
* Execute remember tool
|
|
82
92
|
*/
|
package/dist/orchestrator.js
CHANGED
|
@@ -13,6 +13,7 @@ import { SystemIndexer } from './core/system-indexer.js';
|
|
|
13
13
|
import { BashTool } from './tools/bash.js';
|
|
14
14
|
import { WebTools } from './tools/web.js';
|
|
15
15
|
import { WorkerTools } from './tools/worker.js';
|
|
16
|
+
import { SharedState } from './workers/shared-state.js';
|
|
16
17
|
import { getKnowledgeForPrompt } from './genesis/index.js';
|
|
17
18
|
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
19
|
|
|
@@ -85,6 +86,7 @@ export class MasterOrchestrator {
|
|
|
85
86
|
bashTool;
|
|
86
87
|
webTools;
|
|
87
88
|
workerTools;
|
|
89
|
+
sharedState;
|
|
88
90
|
verboseMode = new Map(); // per-user verbose mode
|
|
89
91
|
constructor(options) {
|
|
90
92
|
this.client = new Anthropic({ apiKey: options.apiKey });
|
|
@@ -108,6 +110,7 @@ export class MasterOrchestrator {
|
|
|
108
110
|
this.bashTool = new BashTool(this.workspaceDir);
|
|
109
111
|
this.webTools = new WebTools();
|
|
110
112
|
this.workerTools = new WorkerTools(this.workspaceDir);
|
|
113
|
+
this.sharedState = new SharedState(this.workspaceDir);
|
|
111
114
|
}
|
|
112
115
|
/**
|
|
113
116
|
* Initialize the orchestrator - indexes filesystem on first call
|
|
@@ -690,11 +693,32 @@ Be specific about what you want done.`,
|
|
|
690
693
|
const maxRetries = 2;
|
|
691
694
|
const id = `worker_${Date.now()}_${++this.jobCounter}`;
|
|
692
695
|
const cwd = workingDir || this.workspaceDir;
|
|
693
|
-
//
|
|
696
|
+
// Search for relevant memories to inject into worker prompt
|
|
697
|
+
let memoryContext = '';
|
|
698
|
+
try {
|
|
699
|
+
const relevantMemories = await this.memory.search(task, { limit: 5, minImportance: 0.3 });
|
|
700
|
+
if (relevantMemories.length > 0) {
|
|
701
|
+
memoryContext = '\n\nRELEVANT KNOWLEDGE FROM MEMORY:\n' +
|
|
702
|
+
relevantMemories.map(m => `[${m.type}] ${m.content}`).join('\n');
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
catch (error) {
|
|
706
|
+
// Memory search failed, continue without it
|
|
707
|
+
console.log('[ORCHESTRATOR] Memory search failed, continuing without context');
|
|
708
|
+
}
|
|
709
|
+
// Get shared state context for worker coordination
|
|
710
|
+
const sharedContext = this.sharedState.getSummaryForWorker(id);
|
|
711
|
+
// Build prompt for worker with memory context and checkpoint instructions
|
|
694
712
|
let prompt = task;
|
|
695
713
|
if (context) {
|
|
696
714
|
prompt = `Context: ${context}\n\nTask: ${task}`;
|
|
697
715
|
}
|
|
716
|
+
if (memoryContext) {
|
|
717
|
+
prompt += memoryContext;
|
|
718
|
+
}
|
|
719
|
+
if (sharedContext) {
|
|
720
|
+
prompt += `\n\nWORKER COORDINATION:${sharedContext}`;
|
|
721
|
+
}
|
|
698
722
|
// Add checkpoint and logging instructions to prevent data loss
|
|
699
723
|
const logFile = `/tmp/worker-${id}-log.txt`;
|
|
700
724
|
prompt += `
|
|
@@ -707,7 +731,21 @@ IMPORTANT - Progress & Logging:
|
|
|
707
731
|
REQUIRED - Log Export:
|
|
708
732
|
At the END of your work, create a final log file at: ${logFile}
|
|
709
733
|
Include: job_id=${id}, timestamp, summary of work done, files modified, key findings.
|
|
710
|
-
This ensures nothing is lost even if your output gets truncated
|
|
734
|
+
This ensures nothing is lost even if your output gets truncated.
|
|
735
|
+
|
|
736
|
+
LEARNING EXPORT (optional but valuable):
|
|
737
|
+
If you discover something important (patterns, user preferences, technical insights),
|
|
738
|
+
end your response with a line like:
|
|
739
|
+
[MEMORY] type=semantic | content=User prefers X over Y for this type of task
|
|
740
|
+
[MEMORY] type=procedural | content=When doing X, always check Y first
|
|
741
|
+
This helps me remember and improve for future tasks.
|
|
742
|
+
|
|
743
|
+
WORKER COORDINATION (optional):
|
|
744
|
+
If you need to share data with other workers or signal completion:
|
|
745
|
+
[SHARE] key=myData | value={"result": "something useful"}
|
|
746
|
+
[SIGNAL] name=step1_complete | data={"files": ["a.ts", "b.ts"]}
|
|
747
|
+
[MESSAGE] to=worker_xyz | content=Please review the changes I made
|
|
748
|
+
This enables parallel workers to coordinate.`;
|
|
711
749
|
console.log(`[ORCHESTRATOR] Delegating to worker ${id}: ${task.slice(0, 80)}...`);
|
|
712
750
|
return new Promise((resolve) => {
|
|
713
751
|
const job = {
|
|
@@ -762,7 +800,7 @@ This ensures nothing is lost even if your output gets truncated.`;
|
|
|
762
800
|
job.output += text;
|
|
763
801
|
}
|
|
764
802
|
});
|
|
765
|
-
child.on('close', (code) => {
|
|
803
|
+
child.on('close', async (code) => {
|
|
766
804
|
clearTimeout(timeout);
|
|
767
805
|
job.status = code === 0 ? 'completed' : 'failed';
|
|
768
806
|
job.endTime = Date.now();
|
|
@@ -771,6 +809,8 @@ This ensures nothing is lost even if your output gets truncated.`;
|
|
|
771
809
|
if (code !== 0 || job.output.length === 0) {
|
|
772
810
|
console.log(`[ORCHESTRATOR] Worker ${id} output: ${job.output.slice(0, 200) || '(empty)'}`);
|
|
773
811
|
}
|
|
812
|
+
// Extract and store memory contributions from worker output
|
|
813
|
+
await this.extractWorkerMemories(job.output, id);
|
|
774
814
|
resolve({
|
|
775
815
|
success: code === 0,
|
|
776
816
|
output: job.output.trim() || '(No output)'
|
|
@@ -796,6 +836,79 @@ This ensures nothing is lost even if your output gets truncated.`;
|
|
|
796
836
|
});
|
|
797
837
|
});
|
|
798
838
|
}
|
|
839
|
+
/**
|
|
840
|
+
* Extract memory and coordination contributions from worker output
|
|
841
|
+
* Workers can contribute using:
|
|
842
|
+
* - [MEMORY] type=X | content=Y
|
|
843
|
+
* - [SHARE] key=X | value=Y
|
|
844
|
+
* - [SIGNAL] name=X | data=Y
|
|
845
|
+
* - [MESSAGE] to=X | content=Y
|
|
846
|
+
*/
|
|
847
|
+
async extractWorkerMemories(output, workerId) {
|
|
848
|
+
let memoryCount = 0;
|
|
849
|
+
let coordCount = 0;
|
|
850
|
+
// Extract memory contributions
|
|
851
|
+
const memoryPattern = /\[MEMORY\]\s*type=(\w+)\s*\|\s*content=(.+?)(?=\n|$)/g;
|
|
852
|
+
let match;
|
|
853
|
+
while ((match = memoryPattern.exec(output)) !== null) {
|
|
854
|
+
const type = match[1].toLowerCase();
|
|
855
|
+
const content = match[2].trim();
|
|
856
|
+
if (!content)
|
|
857
|
+
continue;
|
|
858
|
+
const validTypes = ['episodic', 'semantic', 'procedural'];
|
|
859
|
+
const memType = validTypes.includes(type) ? type : 'semantic';
|
|
860
|
+
try {
|
|
861
|
+
await this.memory.remember(content, {
|
|
862
|
+
type: memType,
|
|
863
|
+
source: `worker:${workerId}`,
|
|
864
|
+
importance: 0.7,
|
|
865
|
+
tags: ['worker-contributed', 'auto-learned']
|
|
866
|
+
});
|
|
867
|
+
memoryCount++;
|
|
868
|
+
}
|
|
869
|
+
catch (error) {
|
|
870
|
+
console.log(`[ORCHESTRATOR] Failed to store worker memory: ${error}`);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
// Extract shared state contributions
|
|
874
|
+
const sharePattern = /\[SHARE\]\s*key=(\w+)\s*\|\s*value=(.+?)(?=\n|$)/g;
|
|
875
|
+
while ((match = sharePattern.exec(output)) !== null) {
|
|
876
|
+
const key = match[1];
|
|
877
|
+
let value = match[2].trim();
|
|
878
|
+
try {
|
|
879
|
+
value = JSON.parse(value);
|
|
880
|
+
}
|
|
881
|
+
catch { /* use as string */ }
|
|
882
|
+
this.sharedState.set(key, value, workerId);
|
|
883
|
+
coordCount++;
|
|
884
|
+
}
|
|
885
|
+
// Extract signal completions
|
|
886
|
+
const signalPattern = /\[SIGNAL\]\s*name=(\w+)(?:\s*\|\s*data=(.+?))?(?=\n|$)/g;
|
|
887
|
+
while ((match = signalPattern.exec(output)) !== null) {
|
|
888
|
+
const name = match[1];
|
|
889
|
+
let data = match[2]?.trim();
|
|
890
|
+
try {
|
|
891
|
+
data = data ? JSON.parse(data) : undefined;
|
|
892
|
+
}
|
|
893
|
+
catch { /* use as string */ }
|
|
894
|
+
this.sharedState.signalComplete(name, workerId, data);
|
|
895
|
+
coordCount++;
|
|
896
|
+
}
|
|
897
|
+
// Extract messages
|
|
898
|
+
const messagePattern = /\[MESSAGE\]\s*to=(\w+)\s*\|\s*content=(.+?)(?=\n|$)/g;
|
|
899
|
+
while ((match = messagePattern.exec(output)) !== null) {
|
|
900
|
+
const to = match[1];
|
|
901
|
+
const content = match[2].trim();
|
|
902
|
+
this.sharedState.sendMessage(workerId, to, 'data', content);
|
|
903
|
+
coordCount++;
|
|
904
|
+
}
|
|
905
|
+
if (memoryCount > 0) {
|
|
906
|
+
console.log(`[ORCHESTRATOR] Extracted ${memoryCount} memory contributions from worker ${workerId}`);
|
|
907
|
+
}
|
|
908
|
+
if (coordCount > 0) {
|
|
909
|
+
console.log(`[ORCHESTRATOR] Extracted ${coordCount} coordination messages from worker ${workerId}`);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
799
912
|
/**
|
|
800
913
|
* Execute remember tool
|
|
801
914
|
*/
|
|
@@ -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
|
+
}
|