@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 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.7'; // 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;
@@ -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
  */
@@ -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
- // Build prompt for worker with checkpoint instructions
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siftd/connect-agent",
3
- "version": "0.2.7",
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",