@siftd/connect-agent 0.2.8 → 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 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.8'; // Should match package.json
13
+ const VERSION = '0.2.9'; // Should match package.json
14
14
  const state = {
15
15
  intervalId: null,
16
16
  runnerId: null,
@@ -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;
@@ -78,8 +79,12 @@ export declare class MasterOrchestrator {
78
79
  */
79
80
  private delegateToWorker;
80
81
  /**
81
- * Extract memory contributions from worker output
82
- * Workers can contribute learnings using: [MEMORY] type=X | content=Y
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
83
88
  */
84
89
  private extractWorkerMemories;
85
90
  /**
@@ -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
@@ -703,6 +706,8 @@ Be specific about what you want done.`,
703
706
  // Memory search failed, continue without it
704
707
  console.log('[ORCHESTRATOR] Memory search failed, continuing without context');
705
708
  }
709
+ // Get shared state context for worker coordination
710
+ const sharedContext = this.sharedState.getSummaryForWorker(id);
706
711
  // Build prompt for worker with memory context and checkpoint instructions
707
712
  let prompt = task;
708
713
  if (context) {
@@ -711,6 +716,9 @@ Be specific about what you want done.`,
711
716
  if (memoryContext) {
712
717
  prompt += memoryContext;
713
718
  }
719
+ if (sharedContext) {
720
+ prompt += `\n\nWORKER COORDINATION:${sharedContext}`;
721
+ }
714
722
  // Add checkpoint and logging instructions to prevent data loss
715
723
  const logFile = `/tmp/worker-${id}-log.txt`;
716
724
  prompt += `
@@ -730,7 +738,14 @@ If you discover something important (patterns, user preferences, technical insig
730
738
  end your response with a line like:
731
739
  [MEMORY] type=semantic | content=User prefers X over Y for this type of task
732
740
  [MEMORY] type=procedural | content=When doing X, always check Y first
733
- This helps me remember and improve for future tasks.`;
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.`;
734
749
  console.log(`[ORCHESTRATOR] Delegating to worker ${id}: ${task.slice(0, 80)}...`);
735
750
  return new Promise((resolve) => {
736
751
  const job = {
@@ -822,37 +837,76 @@ This helps me remember and improve for future tasks.`;
822
837
  });
823
838
  }
824
839
  /**
825
- * Extract memory contributions from worker output
826
- * Workers can contribute learnings using: [MEMORY] type=X | content=Y
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
827
846
  */
828
847
  async extractWorkerMemories(output, workerId) {
829
- // Match lines like: [MEMORY] type=semantic | content=User prefers X
848
+ let memoryCount = 0;
849
+ let coordCount = 0;
850
+ // Extract memory contributions
830
851
  const memoryPattern = /\[MEMORY\]\s*type=(\w+)\s*\|\s*content=(.+?)(?=\n|$)/g;
831
852
  let match;
832
- let count = 0;
833
853
  while ((match = memoryPattern.exec(output)) !== null) {
834
854
  const type = match[1].toLowerCase();
835
855
  const content = match[2].trim();
836
856
  if (!content)
837
857
  continue;
838
- // Validate memory type
839
858
  const validTypes = ['episodic', 'semantic', 'procedural'];
840
859
  const memType = validTypes.includes(type) ? type : 'semantic';
841
860
  try {
842
861
  await this.memory.remember(content, {
843
862
  type: memType,
844
863
  source: `worker:${workerId}`,
845
- importance: 0.7, // Worker-contributed memories are valuable
864
+ importance: 0.7,
846
865
  tags: ['worker-contributed', 'auto-learned']
847
866
  });
848
- count++;
867
+ memoryCount++;
849
868
  }
850
869
  catch (error) {
851
870
  console.log(`[ORCHESTRATOR] Failed to store worker memory: ${error}`);
852
871
  }
853
872
  }
854
- if (count > 0) {
855
- console.log(`[ORCHESTRATOR] Extracted ${count} memory contributions from worker ${workerId}`);
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}`);
856
910
  }
857
911
  }
858
912
  /**
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siftd/connect-agent",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Master orchestrator agent - control Claude Code remotely via web",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",