@sesamespace/hivemind 0.2.0 → 0.3.0

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.
Files changed (72) hide show
  1. package/PLANNING.md +383 -0
  2. package/TASKS.md +60 -0
  3. package/install.sh +187 -0
  4. package/npm-package.json +28 -0
  5. package/package.json +13 -20
  6. package/packages/cli/package.json +23 -0
  7. package/{dist/chunk-DVR2KBL7.js → packages/cli/src/commands/fleet.ts} +50 -30
  8. package/packages/cli/src/commands/init.ts +230 -0
  9. package/{dist/chunk-MBS5A6BZ.js → packages/cli/src/commands/service.ts} +51 -42
  10. package/{dist/chunk-RNK5Q5GR.js → packages/cli/src/commands/start.ts} +12 -14
  11. package/{dist/main.js → packages/cli/src/main.ts} +12 -18
  12. package/packages/cli/tsconfig.json +8 -0
  13. package/packages/memory/Cargo.lock +6480 -0
  14. package/packages/memory/Cargo.toml +21 -0
  15. package/packages/memory/src/context.rs +179 -0
  16. package/packages/memory/src/embeddings.rs +51 -0
  17. package/packages/memory/src/main.rs +626 -0
  18. package/packages/memory/src/promotion.rs +637 -0
  19. package/packages/memory/src/scoring.rs +131 -0
  20. package/packages/memory/src/store.rs +460 -0
  21. package/packages/memory/src/tasks.rs +321 -0
  22. package/packages/runtime/package.json +24 -0
  23. package/packages/runtime/src/__tests__/fleet-integration.test.ts +235 -0
  24. package/packages/runtime/src/__tests__/fleet.test.ts +207 -0
  25. package/packages/runtime/src/__tests__/integration.test.ts +434 -0
  26. package/packages/runtime/src/agent.ts +255 -0
  27. package/packages/runtime/src/config.ts +130 -0
  28. package/packages/runtime/src/context.ts +192 -0
  29. package/packages/runtime/src/fleet/fleet-manager.ts +399 -0
  30. package/packages/runtime/src/fleet/memory-sync.ts +362 -0
  31. package/packages/runtime/src/fleet/primary-client.ts +285 -0
  32. package/packages/runtime/src/fleet/worker-protocol.ts +158 -0
  33. package/packages/runtime/src/fleet/worker-server.ts +246 -0
  34. package/packages/runtime/src/index.ts +57 -0
  35. package/packages/runtime/src/llm-client.ts +65 -0
  36. package/packages/runtime/src/memory-client.ts +309 -0
  37. package/packages/runtime/src/pipeline.ts +151 -0
  38. package/packages/runtime/src/prompt.ts +173 -0
  39. package/packages/runtime/src/sesame.ts +174 -0
  40. package/{dist/start.js → packages/runtime/src/start.ts} +7 -9
  41. package/packages/runtime/src/task-engine.ts +113 -0
  42. package/packages/runtime/src/worker.ts +339 -0
  43. package/packages/runtime/tsconfig.json +8 -0
  44. package/pnpm-workspace.yaml +2 -0
  45. package/run-aidan.sh +23 -0
  46. package/scripts/bootstrap.sh +196 -0
  47. package/scripts/build-npm.sh +94 -0
  48. package/scripts/com.hivemind.agent.plist +44 -0
  49. package/scripts/com.hivemind.memory.plist +31 -0
  50. package/tsconfig.json +22 -0
  51. package/tsup.config.ts +28 -0
  52. package/dist/chunk-2I2O6X5D.js +0 -1408
  53. package/dist/chunk-2I2O6X5D.js.map +0 -1
  54. package/dist/chunk-DVR2KBL7.js.map +0 -1
  55. package/dist/chunk-MBS5A6BZ.js.map +0 -1
  56. package/dist/chunk-NVJ424TB.js +0 -731
  57. package/dist/chunk-NVJ424TB.js.map +0 -1
  58. package/dist/chunk-RNK5Q5GR.js.map +0 -1
  59. package/dist/chunk-XNOWVLXD.js +0 -160
  60. package/dist/chunk-XNOWVLXD.js.map +0 -1
  61. package/dist/commands/fleet.js +0 -9
  62. package/dist/commands/fleet.js.map +0 -1
  63. package/dist/commands/init.js +0 -7
  64. package/dist/commands/init.js.map +0 -1
  65. package/dist/commands/service.js +0 -7
  66. package/dist/commands/service.js.map +0 -1
  67. package/dist/commands/start.js +0 -9
  68. package/dist/commands/start.js.map +0 -1
  69. package/dist/index.js +0 -41
  70. package/dist/index.js.map +0 -1
  71. package/dist/main.js.map +0 -1
  72. package/dist/start.js.map +0 -1
@@ -0,0 +1,399 @@
1
+ /**
2
+ * Fleet Manager — high-level orchestration layer on top of PrimaryClient.
3
+ *
4
+ * Provides:
5
+ * - Worker provisioning (register by URL)
6
+ * - Context assignment and migration between workers
7
+ * - Aggregate fleet status / dashboard data
8
+ * - Worker discovery and removal
9
+ */
10
+
11
+ import { PrimaryClient } from "./primary-client.js";
12
+ import {
13
+ WORKER_ROUTES,
14
+ HEALTH_TIMEOUT_MS,
15
+ } from "./worker-protocol.js";
16
+ import type {
17
+ WorkerInfo,
18
+ WorkerHealthResponse,
19
+ WorkerHealthStatus,
20
+ WorkerCapabilities,
21
+ ContextAssignResponse,
22
+ WorkerStatus,
23
+ WorkerActivity,
24
+ } from "./worker-protocol.js";
25
+
26
+ // --- Dashboard Types ---
27
+
28
+ export interface WorkerSummary {
29
+ id: string;
30
+ url: string;
31
+ health: WorkerHealthStatus;
32
+ contexts: string[];
33
+ current_task: string | null;
34
+ activity: WorkerActivity;
35
+ uptime_seconds: number | null;
36
+ capabilities: WorkerCapabilities;
37
+ last_heartbeat: string;
38
+ }
39
+
40
+ export interface FleetDashboard {
41
+ total_workers: number;
42
+ healthy: number;
43
+ degraded: number;
44
+ unreachable: number;
45
+ total_contexts: number;
46
+ workers: WorkerSummary[];
47
+ unassigned_contexts: string[];
48
+ generated_at: string;
49
+ }
50
+
51
+ export interface MigrationResult {
52
+ context_name: string;
53
+ from_worker: string;
54
+ to_worker: string;
55
+ success: boolean;
56
+ reason?: string;
57
+ }
58
+
59
+ export class FleetManager {
60
+ private primary: PrimaryClient;
61
+ private latestHealth: Map<string, WorkerHealthResponse> = new Map();
62
+ private latestStatus: Map<string, WorkerStatus> = new Map();
63
+ private knownContexts: Set<string> = new Set();
64
+
65
+ constructor(primary?: PrimaryClient) {
66
+ this.primary = primary ?? new PrimaryClient();
67
+ }
68
+
69
+ getPrimary(): PrimaryClient {
70
+ return this.primary;
71
+ }
72
+
73
+ // --- Worker Provisioning ---
74
+
75
+ /**
76
+ * Register a new worker by URL. Probes the worker's health endpoint
77
+ * to discover capabilities, then registers it with the Primary.
78
+ */
79
+ async addWorker(url: string): Promise<WorkerInfo> {
80
+ // Normalise trailing slash
81
+ const baseUrl = url.replace(/\/+$/, "");
82
+
83
+ // Probe worker health to discover capabilities
84
+ const capabilities = await this.probeWorker(baseUrl);
85
+
86
+ const reg = this.primary.handleRegistration({ url: baseUrl, capabilities });
87
+
88
+ const worker = this.primary.getWorker(reg.worker_id);
89
+ if (!worker) {
90
+ throw new Error("Worker registered but not found in registry");
91
+ }
92
+
93
+ return worker;
94
+ }
95
+
96
+ /** Remove a worker from the fleet, unassigning its contexts first. */
97
+ async removeWorker(workerId: string): Promise<boolean> {
98
+ const worker = this.primary.getWorker(workerId);
99
+ if (!worker) return false;
100
+
101
+ // Unassign all contexts from this worker
102
+ for (const ctx of [...worker.assigned_contexts]) {
103
+ await this.primary.unassignContext(workerId, ctx);
104
+ }
105
+
106
+ return this.primary.deregister(workerId);
107
+ }
108
+
109
+ // --- Context Assignment ---
110
+
111
+ /** Assign a context to a specific worker. */
112
+ async assignContext(
113
+ workerId: string,
114
+ contextName: string,
115
+ description = "",
116
+ ): Promise<ContextAssignResponse> {
117
+ const result = await this.primary.assignContext(workerId, contextName, description);
118
+ if (result.accepted) {
119
+ this.knownContexts.add(contextName);
120
+ }
121
+ return result;
122
+ }
123
+
124
+ /** Migrate a context from one worker to another. */
125
+ async migrateContext(
126
+ contextName: string,
127
+ toWorkerId: string,
128
+ ): Promise<MigrationResult> {
129
+ const fromWorker = this.primary.findWorkerForContext(contextName);
130
+
131
+ if (!fromWorker) {
132
+ // Context isn't assigned anywhere — just assign to target
133
+ const resp = await this.primary.assignContext(toWorkerId, contextName);
134
+ return {
135
+ context_name: contextName,
136
+ from_worker: "(none)",
137
+ to_worker: toWorkerId,
138
+ success: resp.accepted,
139
+ reason: resp.accepted ? undefined : resp.reason,
140
+ };
141
+ }
142
+
143
+ if (fromWorker.id === toWorkerId) {
144
+ return {
145
+ context_name: contextName,
146
+ from_worker: fromWorker.id,
147
+ to_worker: toWorkerId,
148
+ success: false,
149
+ reason: "Context already assigned to this worker",
150
+ };
151
+ }
152
+
153
+ // Assign to new worker first, then unassign from old
154
+ const assignResult = await this.primary.assignContext(toWorkerId, contextName);
155
+ if (!assignResult.accepted) {
156
+ return {
157
+ context_name: contextName,
158
+ from_worker: fromWorker.id,
159
+ to_worker: toWorkerId,
160
+ success: false,
161
+ reason: assignResult.reason ?? "Target worker rejected assignment",
162
+ };
163
+ }
164
+
165
+ await this.primary.unassignContext(fromWorker.id, contextName);
166
+
167
+ return {
168
+ context_name: contextName,
169
+ from_worker: fromWorker.id,
170
+ to_worker: toWorkerId,
171
+ success: true,
172
+ };
173
+ }
174
+
175
+ // --- Status & Health ---
176
+
177
+ /** Refresh health for all workers. */
178
+ async refreshHealth(): Promise<void> {
179
+ const workers = this.primary.getWorkers();
180
+
181
+ const checks = workers.map(async (w) => {
182
+ const health = await this.primary.checkHealth(w.id);
183
+ if (health) {
184
+ this.latestHealth.set(w.id, health);
185
+ } else {
186
+ // Mark unreachable but keep stale data
187
+ this.latestHealth.set(w.id, {
188
+ worker_id: w.id,
189
+ status: "unreachable",
190
+ uptime_seconds: 0,
191
+ assigned_contexts: w.assigned_contexts,
192
+ active_context: null,
193
+ memory_daemon_ok: false,
194
+ ollama_ok: false,
195
+ });
196
+ }
197
+ });
198
+
199
+ await Promise.all(checks);
200
+ }
201
+
202
+ /** Record a status report (called when worker POSTs status to Primary). */
203
+ recordStatus(workerId: string, status: WorkerStatus): void {
204
+ this.latestStatus.set(workerId, status);
205
+ }
206
+
207
+ /** Get the full fleet dashboard. */
208
+ async getDashboard(): Promise<FleetDashboard> {
209
+ await this.refreshHealth();
210
+
211
+ const workers = this.primary.getWorkers();
212
+ let healthy = 0;
213
+ let degraded = 0;
214
+ let unreachable = 0;
215
+ let totalContexts = 0;
216
+
217
+ const workerSummaries: WorkerSummary[] = workers.map((w) => {
218
+ const health = this.latestHealth.get(w.id);
219
+ const status = this.latestStatus.get(w.id);
220
+
221
+ const healthStatus: WorkerHealthStatus = health?.status ?? "unreachable";
222
+ if (healthStatus === "healthy") healthy++;
223
+ else if (healthStatus === "degraded") degraded++;
224
+ else unreachable++;
225
+
226
+ totalContexts += w.assigned_contexts.length;
227
+
228
+ return {
229
+ id: w.id,
230
+ url: w.url,
231
+ health: healthStatus,
232
+ contexts: w.assigned_contexts,
233
+ current_task: status?.current_task ?? null,
234
+ activity: status?.activity ?? "idle",
235
+ uptime_seconds: health?.uptime_seconds ?? null,
236
+ capabilities: w.capabilities,
237
+ last_heartbeat: w.last_heartbeat,
238
+ };
239
+ });
240
+
241
+ // Determine unassigned contexts
242
+ const assignedContexts = new Set(workers.flatMap((w) => w.assigned_contexts));
243
+ const unassigned = [...this.knownContexts].filter((c) => !assignedContexts.has(c));
244
+
245
+ return {
246
+ total_workers: workers.length,
247
+ healthy,
248
+ degraded,
249
+ unreachable,
250
+ total_contexts: totalContexts,
251
+ workers: workerSummaries,
252
+ unassigned_contexts: unassigned,
253
+ generated_at: new Date().toISOString(),
254
+ };
255
+ }
256
+
257
+ /** Get a quick status summary without refreshing health. */
258
+ getStatusSnapshot(): FleetDashboard {
259
+ const workers = this.primary.getWorkers();
260
+ let healthy = 0;
261
+ let degraded = 0;
262
+ let unreachable = 0;
263
+ let totalContexts = 0;
264
+
265
+ const workerSummaries: WorkerSummary[] = workers.map((w) => {
266
+ const health = this.latestHealth.get(w.id);
267
+ const status = this.latestStatus.get(w.id);
268
+
269
+ const healthStatus: WorkerHealthStatus = health?.status ?? "unreachable";
270
+ if (healthStatus === "healthy") healthy++;
271
+ else if (healthStatus === "degraded") degraded++;
272
+ else unreachable++;
273
+
274
+ totalContexts += w.assigned_contexts.length;
275
+
276
+ return {
277
+ id: w.id,
278
+ url: w.url,
279
+ health: healthStatus,
280
+ contexts: w.assigned_contexts,
281
+ current_task: status?.current_task ?? null,
282
+ activity: status?.activity ?? "idle",
283
+ uptime_seconds: health?.uptime_seconds ?? null,
284
+ capabilities: w.capabilities,
285
+ last_heartbeat: w.last_heartbeat,
286
+ };
287
+ });
288
+
289
+ const assignedContexts = new Set(workers.flatMap((w) => w.assigned_contexts));
290
+ const unassigned = [...this.knownContexts].filter((c) => !assignedContexts.has(c));
291
+
292
+ return {
293
+ total_workers: workers.length,
294
+ healthy,
295
+ degraded,
296
+ unreachable,
297
+ total_contexts: totalContexts,
298
+ workers: workerSummaries,
299
+ unassigned_contexts: unassigned,
300
+ generated_at: new Date().toISOString(),
301
+ };
302
+ }
303
+
304
+ // --- Worker Discovery ---
305
+
306
+ /**
307
+ * Scan a list of candidate URLs for workers. Returns the URLs
308
+ * that responded to a health probe. Useful for auto-detecting
309
+ * workers on a local network.
310
+ */
311
+ async discoverWorkers(candidateUrls: string[]): Promise<string[]> {
312
+ const found: string[] = [];
313
+
314
+ const probes = candidateUrls.map(async (url) => {
315
+ const baseUrl = url.replace(/\/+$/, "");
316
+ try {
317
+ const controller = new AbortController();
318
+ const timeout = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
319
+
320
+ const resp = await fetch(`${baseUrl}${WORKER_ROUTES.health}`, {
321
+ signal: controller.signal,
322
+ });
323
+ clearTimeout(timeout);
324
+
325
+ if (resp.ok) {
326
+ found.push(baseUrl);
327
+ }
328
+ } catch {
329
+ // Not reachable, skip
330
+ }
331
+ });
332
+
333
+ await Promise.all(probes);
334
+ return found;
335
+ }
336
+
337
+ /**
338
+ * Scan a subnet-style range for workers. Generates URLs for
339
+ * a base IP with ports and probes them.
340
+ * Example: scanSubnet("192.168.1", [10, 11, 12], 3100)
341
+ */
342
+ async scanSubnet(
343
+ baseIp: string,
344
+ hostIds: number[],
345
+ port: number,
346
+ ): Promise<string[]> {
347
+ const urls = hostIds.map((id) => `http://${baseIp}.${id}:${port}`);
348
+ return this.discoverWorkers(urls);
349
+ }
350
+
351
+ /** Track a context name so it appears in unassigned lists. */
352
+ registerContext(contextName: string): void {
353
+ this.knownContexts.add(contextName);
354
+ }
355
+
356
+ /** Start health polling (delegates to PrimaryClient). */
357
+ startHealthPolling(intervalMs?: number): void {
358
+ this.primary.startHealthPolling(intervalMs);
359
+ }
360
+
361
+ /** Stop health polling. */
362
+ stopHealthPolling(): void {
363
+ this.primary.stopHealthPolling();
364
+ }
365
+
366
+ // --- Internal ---
367
+
368
+ private async probeWorker(baseUrl: string): Promise<WorkerCapabilities> {
369
+ try {
370
+ const controller = new AbortController();
371
+ const timeout = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
372
+
373
+ const resp = await fetch(`${baseUrl}${WORKER_ROUTES.health}`, {
374
+ signal: controller.signal,
375
+ });
376
+ clearTimeout(timeout);
377
+
378
+ if (resp.ok) {
379
+ const health = (await resp.json()) as WorkerHealthResponse;
380
+ return {
381
+ max_contexts: health.assigned_contexts.length + 4, // assume capacity
382
+ has_ollama: health.ollama_ok,
383
+ has_memory_daemon: health.memory_daemon_ok,
384
+ available_models: [],
385
+ };
386
+ }
387
+ } catch {
388
+ // Fall through to defaults
389
+ }
390
+
391
+ // Return default capabilities if probe fails
392
+ return {
393
+ max_contexts: 4,
394
+ has_ollama: false,
395
+ has_memory_daemon: false,
396
+ available_models: [],
397
+ };
398
+ }
399
+ }