@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.
- package/PLANNING.md +383 -0
- package/TASKS.md +60 -0
- package/install.sh +187 -0
- package/npm-package.json +28 -0
- package/package.json +13 -20
- package/packages/cli/package.json +23 -0
- package/{dist/chunk-DVR2KBL7.js → packages/cli/src/commands/fleet.ts} +50 -30
- package/packages/cli/src/commands/init.ts +230 -0
- package/{dist/chunk-MBS5A6BZ.js → packages/cli/src/commands/service.ts} +51 -42
- package/{dist/chunk-RNK5Q5GR.js → packages/cli/src/commands/start.ts} +12 -14
- package/{dist/main.js → packages/cli/src/main.ts} +12 -18
- package/packages/cli/tsconfig.json +8 -0
- package/packages/memory/Cargo.lock +6480 -0
- package/packages/memory/Cargo.toml +21 -0
- package/packages/memory/src/context.rs +179 -0
- package/packages/memory/src/embeddings.rs +51 -0
- package/packages/memory/src/main.rs +626 -0
- package/packages/memory/src/promotion.rs +637 -0
- package/packages/memory/src/scoring.rs +131 -0
- package/packages/memory/src/store.rs +460 -0
- package/packages/memory/src/tasks.rs +321 -0
- package/packages/runtime/package.json +24 -0
- package/packages/runtime/src/__tests__/fleet-integration.test.ts +235 -0
- package/packages/runtime/src/__tests__/fleet.test.ts +207 -0
- package/packages/runtime/src/__tests__/integration.test.ts +434 -0
- package/packages/runtime/src/agent.ts +255 -0
- package/packages/runtime/src/config.ts +130 -0
- package/packages/runtime/src/context.ts +192 -0
- package/packages/runtime/src/fleet/fleet-manager.ts +399 -0
- package/packages/runtime/src/fleet/memory-sync.ts +362 -0
- package/packages/runtime/src/fleet/primary-client.ts +285 -0
- package/packages/runtime/src/fleet/worker-protocol.ts +158 -0
- package/packages/runtime/src/fleet/worker-server.ts +246 -0
- package/packages/runtime/src/index.ts +57 -0
- package/packages/runtime/src/llm-client.ts +65 -0
- package/packages/runtime/src/memory-client.ts +309 -0
- package/packages/runtime/src/pipeline.ts +151 -0
- package/packages/runtime/src/prompt.ts +173 -0
- package/packages/runtime/src/sesame.ts +174 -0
- package/{dist/start.js → packages/runtime/src/start.ts} +7 -9
- package/packages/runtime/src/task-engine.ts +113 -0
- package/packages/runtime/src/worker.ts +339 -0
- package/packages/runtime/tsconfig.json +8 -0
- package/pnpm-workspace.yaml +2 -0
- package/run-aidan.sh +23 -0
- package/scripts/bootstrap.sh +196 -0
- package/scripts/build-npm.sh +94 -0
- package/scripts/com.hivemind.agent.plist +44 -0
- package/scripts/com.hivemind.memory.plist +31 -0
- package/tsconfig.json +22 -0
- package/tsup.config.ts +28 -0
- package/dist/chunk-2I2O6X5D.js +0 -1408
- package/dist/chunk-2I2O6X5D.js.map +0 -1
- package/dist/chunk-DVR2KBL7.js.map +0 -1
- package/dist/chunk-MBS5A6BZ.js.map +0 -1
- package/dist/chunk-NVJ424TB.js +0 -731
- package/dist/chunk-NVJ424TB.js.map +0 -1
- package/dist/chunk-RNK5Q5GR.js.map +0 -1
- package/dist/chunk-XNOWVLXD.js +0 -160
- package/dist/chunk-XNOWVLXD.js.map +0 -1
- package/dist/commands/fleet.js +0 -9
- package/dist/commands/fleet.js.map +0 -1
- package/dist/commands/init.js +0 -7
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/service.js +0 -7
- package/dist/commands/service.js.map +0 -1
- package/dist/commands/start.js +0 -9
- package/dist/commands/start.js.map +0 -1
- package/dist/index.js +0 -41
- package/dist/index.js.map +0 -1
- package/dist/main.js.map +0 -1
- package/dist/start.js.map +0 -1
package/dist/chunk-NVJ424TB.js
DELETED
|
@@ -1,731 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DEFAULT_HEALTH_INTERVAL_MS,
|
|
3
|
-
DEFAULT_SYNC_INTERVAL_MS,
|
|
4
|
-
HEALTH_TIMEOUT_MS,
|
|
5
|
-
PRIMARY_ROUTES,
|
|
6
|
-
WORKER_ROUTES
|
|
7
|
-
} from "./chunk-2I2O6X5D.js";
|
|
8
|
-
|
|
9
|
-
// packages/runtime/src/fleet/primary-client.ts
|
|
10
|
-
var PrimaryClient = class {
|
|
11
|
-
workers = /* @__PURE__ */ new Map();
|
|
12
|
-
healthTimer = null;
|
|
13
|
-
nextId = 1;
|
|
14
|
-
onSyncPullCallback = null;
|
|
15
|
-
/** All registered workers. */
|
|
16
|
-
getWorkers() {
|
|
17
|
-
return Array.from(this.workers.values());
|
|
18
|
-
}
|
|
19
|
-
/** Get a single worker by ID. */
|
|
20
|
-
getWorker(workerId) {
|
|
21
|
-
return this.workers.get(workerId);
|
|
22
|
-
}
|
|
23
|
-
/** Find which worker owns a context, if any. */
|
|
24
|
-
findWorkerForContext(contextName) {
|
|
25
|
-
for (const w of this.workers.values()) {
|
|
26
|
-
if (w.assigned_contexts.includes(contextName)) return w;
|
|
27
|
-
}
|
|
28
|
-
return void 0;
|
|
29
|
-
}
|
|
30
|
-
// --- Registration (called when Worker POSTs to Primary) ---
|
|
31
|
-
handleRegistration(req) {
|
|
32
|
-
const id = `worker-${this.nextId++}`;
|
|
33
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
34
|
-
const info = {
|
|
35
|
-
id,
|
|
36
|
-
url: req.url,
|
|
37
|
-
capabilities: req.capabilities,
|
|
38
|
-
assigned_contexts: [],
|
|
39
|
-
registered_at: now,
|
|
40
|
-
last_heartbeat: now
|
|
41
|
-
};
|
|
42
|
-
this.workers.set(id, info);
|
|
43
|
-
return { worker_id: id, registered_at: now };
|
|
44
|
-
}
|
|
45
|
-
/** Remove a worker from the registry. */
|
|
46
|
-
deregister(workerId) {
|
|
47
|
-
return this.workers.delete(workerId);
|
|
48
|
-
}
|
|
49
|
-
// --- Health Checking ---
|
|
50
|
-
/** Poll a single worker's health endpoint. */
|
|
51
|
-
async checkHealth(workerId) {
|
|
52
|
-
const worker = this.workers.get(workerId);
|
|
53
|
-
if (!worker) return null;
|
|
54
|
-
try {
|
|
55
|
-
const controller = new AbortController();
|
|
56
|
-
const timeout = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
|
|
57
|
-
const resp = await fetch(`${worker.url}${WORKER_ROUTES.health}`, {
|
|
58
|
-
signal: controller.signal
|
|
59
|
-
});
|
|
60
|
-
clearTimeout(timeout);
|
|
61
|
-
if (!resp.ok) {
|
|
62
|
-
this.markUnreachable(workerId);
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
const health = await resp.json();
|
|
66
|
-
worker.last_heartbeat = (/* @__PURE__ */ new Date()).toISOString();
|
|
67
|
-
return health;
|
|
68
|
-
} catch {
|
|
69
|
-
this.markUnreachable(workerId);
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
/** Poll all workers and return their statuses. */
|
|
74
|
-
async checkAllHealth() {
|
|
75
|
-
const results = /* @__PURE__ */ new Map();
|
|
76
|
-
const checks = Array.from(this.workers.keys()).map(async (id) => {
|
|
77
|
-
const health = await this.checkHealth(id);
|
|
78
|
-
results.set(id, health?.status ?? "unreachable");
|
|
79
|
-
});
|
|
80
|
-
await Promise.all(checks);
|
|
81
|
-
return results;
|
|
82
|
-
}
|
|
83
|
-
/** Start periodic health polling. */
|
|
84
|
-
startHealthPolling(intervalMs = DEFAULT_HEALTH_INTERVAL_MS) {
|
|
85
|
-
this.stopHealthPolling();
|
|
86
|
-
this.healthTimer = setInterval(() => {
|
|
87
|
-
this.checkAllHealth().catch(() => {
|
|
88
|
-
});
|
|
89
|
-
}, intervalMs);
|
|
90
|
-
}
|
|
91
|
-
/** Stop periodic health polling. */
|
|
92
|
-
stopHealthPolling() {
|
|
93
|
-
if (this.healthTimer) {
|
|
94
|
-
clearInterval(this.healthTimer);
|
|
95
|
-
this.healthTimer = null;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
// --- Context Assignment ---
|
|
99
|
-
/** Assign a context to a worker. */
|
|
100
|
-
async assignContext(workerId, contextName, contextDescription = "") {
|
|
101
|
-
const worker = this.workers.get(workerId);
|
|
102
|
-
if (!worker) {
|
|
103
|
-
return { context_name: contextName, accepted: false, reason: "Worker not found" };
|
|
104
|
-
}
|
|
105
|
-
if (worker.assigned_contexts.length >= worker.capabilities.max_contexts) {
|
|
106
|
-
return { context_name: contextName, accepted: false, reason: "Worker at capacity" };
|
|
107
|
-
}
|
|
108
|
-
const body = {
|
|
109
|
-
context_name: contextName,
|
|
110
|
-
context_description: contextDescription
|
|
111
|
-
};
|
|
112
|
-
try {
|
|
113
|
-
const resp = await fetch(`${worker.url}${WORKER_ROUTES.assign}`, {
|
|
114
|
-
method: "POST",
|
|
115
|
-
headers: { "Content-Type": "application/json" },
|
|
116
|
-
body: JSON.stringify(body)
|
|
117
|
-
});
|
|
118
|
-
if (!resp.ok) {
|
|
119
|
-
const text = await resp.text();
|
|
120
|
-
return { context_name: contextName, accepted: false, reason: `Worker rejected: ${text}` };
|
|
121
|
-
}
|
|
122
|
-
const result = await resp.json();
|
|
123
|
-
if (result.accepted) {
|
|
124
|
-
worker.assigned_contexts.push(contextName);
|
|
125
|
-
}
|
|
126
|
-
return result;
|
|
127
|
-
} catch (err) {
|
|
128
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
129
|
-
return { context_name: contextName, accepted: false, reason: `Request failed: ${msg}` };
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
/** Unassign a context from a worker. */
|
|
133
|
-
async unassignContext(workerId, contextName) {
|
|
134
|
-
const worker = this.workers.get(workerId);
|
|
135
|
-
if (!worker) return false;
|
|
136
|
-
try {
|
|
137
|
-
const resp = await fetch(`${worker.url}${WORKER_ROUTES.unassign(contextName)}`, {
|
|
138
|
-
method: "DELETE"
|
|
139
|
-
});
|
|
140
|
-
if (resp.ok) {
|
|
141
|
-
worker.assigned_contexts = worker.assigned_contexts.filter((c) => c !== contextName);
|
|
142
|
-
return true;
|
|
143
|
-
}
|
|
144
|
-
return false;
|
|
145
|
-
} catch {
|
|
146
|
-
return false;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
// --- Status Collection ---
|
|
150
|
-
/** Handle an incoming status report from a Worker. */
|
|
151
|
-
handleStatusReport(workerId, report) {
|
|
152
|
-
const worker = this.workers.get(workerId);
|
|
153
|
-
if (!worker) return null;
|
|
154
|
-
worker.last_heartbeat = (/* @__PURE__ */ new Date()).toISOString();
|
|
155
|
-
return {
|
|
156
|
-
worker_id: workerId,
|
|
157
|
-
activity: report.activity,
|
|
158
|
-
current_context: report.current_context,
|
|
159
|
-
current_task: report.current_task,
|
|
160
|
-
error: report.error,
|
|
161
|
-
reported_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
// --- Memory Sync ---
|
|
165
|
-
/** Register a handler for incoming sync pull requests from Workers. */
|
|
166
|
-
onSyncPull(cb) {
|
|
167
|
-
this.onSyncPullCallback = cb;
|
|
168
|
-
}
|
|
169
|
-
/** Handle an incoming sync pull (Worker sends L3 knowledge to Primary). */
|
|
170
|
-
async handleSyncPull(req) {
|
|
171
|
-
if (!this.onSyncPullCallback) {
|
|
172
|
-
return { accepted: 0, rejected: req.entries.length };
|
|
173
|
-
}
|
|
174
|
-
return this.onSyncPullCallback(req);
|
|
175
|
-
}
|
|
176
|
-
/** Push Global context updates to a specific worker. */
|
|
177
|
-
async pushSyncToWorker(workerId, payload) {
|
|
178
|
-
const worker = this.workers.get(workerId);
|
|
179
|
-
if (!worker) return null;
|
|
180
|
-
try {
|
|
181
|
-
const controller = new AbortController();
|
|
182
|
-
const timeout = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
|
|
183
|
-
const resp = await fetch(`${worker.url}${WORKER_ROUTES.syncPush}`, {
|
|
184
|
-
method: "POST",
|
|
185
|
-
headers: { "Content-Type": "application/json" },
|
|
186
|
-
body: JSON.stringify(payload),
|
|
187
|
-
signal: controller.signal
|
|
188
|
-
});
|
|
189
|
-
clearTimeout(timeout);
|
|
190
|
-
if (!resp.ok) return null;
|
|
191
|
-
return await resp.json();
|
|
192
|
-
} catch {
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
/** Push Global context updates to all registered workers. */
|
|
197
|
-
async pushSyncToAll(payload) {
|
|
198
|
-
const results = /* @__PURE__ */ new Map();
|
|
199
|
-
const pushes = Array.from(this.workers.keys()).map(async (id) => {
|
|
200
|
-
const result = await this.pushSyncToWorker(id, payload);
|
|
201
|
-
results.set(id, result);
|
|
202
|
-
});
|
|
203
|
-
await Promise.all(pushes);
|
|
204
|
-
return results;
|
|
205
|
-
}
|
|
206
|
-
// --- Internal ---
|
|
207
|
-
markUnreachable(workerId) {
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
// packages/runtime/src/fleet/fleet-manager.ts
|
|
212
|
-
var FleetManager = class {
|
|
213
|
-
primary;
|
|
214
|
-
latestHealth = /* @__PURE__ */ new Map();
|
|
215
|
-
latestStatus = /* @__PURE__ */ new Map();
|
|
216
|
-
knownContexts = /* @__PURE__ */ new Set();
|
|
217
|
-
constructor(primary) {
|
|
218
|
-
this.primary = primary ?? new PrimaryClient();
|
|
219
|
-
}
|
|
220
|
-
getPrimary() {
|
|
221
|
-
return this.primary;
|
|
222
|
-
}
|
|
223
|
-
// --- Worker Provisioning ---
|
|
224
|
-
/**
|
|
225
|
-
* Register a new worker by URL. Probes the worker's health endpoint
|
|
226
|
-
* to discover capabilities, then registers it with the Primary.
|
|
227
|
-
*/
|
|
228
|
-
async addWorker(url) {
|
|
229
|
-
const baseUrl = url.replace(/\/+$/, "");
|
|
230
|
-
const capabilities = await this.probeWorker(baseUrl);
|
|
231
|
-
const reg = this.primary.handleRegistration({ url: baseUrl, capabilities });
|
|
232
|
-
const worker = this.primary.getWorker(reg.worker_id);
|
|
233
|
-
if (!worker) {
|
|
234
|
-
throw new Error("Worker registered but not found in registry");
|
|
235
|
-
}
|
|
236
|
-
return worker;
|
|
237
|
-
}
|
|
238
|
-
/** Remove a worker from the fleet, unassigning its contexts first. */
|
|
239
|
-
async removeWorker(workerId) {
|
|
240
|
-
const worker = this.primary.getWorker(workerId);
|
|
241
|
-
if (!worker) return false;
|
|
242
|
-
for (const ctx of [...worker.assigned_contexts]) {
|
|
243
|
-
await this.primary.unassignContext(workerId, ctx);
|
|
244
|
-
}
|
|
245
|
-
return this.primary.deregister(workerId);
|
|
246
|
-
}
|
|
247
|
-
// --- Context Assignment ---
|
|
248
|
-
/** Assign a context to a specific worker. */
|
|
249
|
-
async assignContext(workerId, contextName, description = "") {
|
|
250
|
-
const result = await this.primary.assignContext(workerId, contextName, description);
|
|
251
|
-
if (result.accepted) {
|
|
252
|
-
this.knownContexts.add(contextName);
|
|
253
|
-
}
|
|
254
|
-
return result;
|
|
255
|
-
}
|
|
256
|
-
/** Migrate a context from one worker to another. */
|
|
257
|
-
async migrateContext(contextName, toWorkerId) {
|
|
258
|
-
const fromWorker = this.primary.findWorkerForContext(contextName);
|
|
259
|
-
if (!fromWorker) {
|
|
260
|
-
const resp = await this.primary.assignContext(toWorkerId, contextName);
|
|
261
|
-
return {
|
|
262
|
-
context_name: contextName,
|
|
263
|
-
from_worker: "(none)",
|
|
264
|
-
to_worker: toWorkerId,
|
|
265
|
-
success: resp.accepted,
|
|
266
|
-
reason: resp.accepted ? void 0 : resp.reason
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
if (fromWorker.id === toWorkerId) {
|
|
270
|
-
return {
|
|
271
|
-
context_name: contextName,
|
|
272
|
-
from_worker: fromWorker.id,
|
|
273
|
-
to_worker: toWorkerId,
|
|
274
|
-
success: false,
|
|
275
|
-
reason: "Context already assigned to this worker"
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
const assignResult = await this.primary.assignContext(toWorkerId, contextName);
|
|
279
|
-
if (!assignResult.accepted) {
|
|
280
|
-
return {
|
|
281
|
-
context_name: contextName,
|
|
282
|
-
from_worker: fromWorker.id,
|
|
283
|
-
to_worker: toWorkerId,
|
|
284
|
-
success: false,
|
|
285
|
-
reason: assignResult.reason ?? "Target worker rejected assignment"
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
await this.primary.unassignContext(fromWorker.id, contextName);
|
|
289
|
-
return {
|
|
290
|
-
context_name: contextName,
|
|
291
|
-
from_worker: fromWorker.id,
|
|
292
|
-
to_worker: toWorkerId,
|
|
293
|
-
success: true
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
// --- Status & Health ---
|
|
297
|
-
/** Refresh health for all workers. */
|
|
298
|
-
async refreshHealth() {
|
|
299
|
-
const workers = this.primary.getWorkers();
|
|
300
|
-
const checks = workers.map(async (w) => {
|
|
301
|
-
const health = await this.primary.checkHealth(w.id);
|
|
302
|
-
if (health) {
|
|
303
|
-
this.latestHealth.set(w.id, health);
|
|
304
|
-
} else {
|
|
305
|
-
this.latestHealth.set(w.id, {
|
|
306
|
-
worker_id: w.id,
|
|
307
|
-
status: "unreachable",
|
|
308
|
-
uptime_seconds: 0,
|
|
309
|
-
assigned_contexts: w.assigned_contexts,
|
|
310
|
-
active_context: null,
|
|
311
|
-
memory_daemon_ok: false,
|
|
312
|
-
ollama_ok: false
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
|
-
await Promise.all(checks);
|
|
317
|
-
}
|
|
318
|
-
/** Record a status report (called when worker POSTs status to Primary). */
|
|
319
|
-
recordStatus(workerId, status) {
|
|
320
|
-
this.latestStatus.set(workerId, status);
|
|
321
|
-
}
|
|
322
|
-
/** Get the full fleet dashboard. */
|
|
323
|
-
async getDashboard() {
|
|
324
|
-
await this.refreshHealth();
|
|
325
|
-
const workers = this.primary.getWorkers();
|
|
326
|
-
let healthy = 0;
|
|
327
|
-
let degraded = 0;
|
|
328
|
-
let unreachable = 0;
|
|
329
|
-
let totalContexts = 0;
|
|
330
|
-
const workerSummaries = workers.map((w) => {
|
|
331
|
-
const health = this.latestHealth.get(w.id);
|
|
332
|
-
const status = this.latestStatus.get(w.id);
|
|
333
|
-
const healthStatus = health?.status ?? "unreachable";
|
|
334
|
-
if (healthStatus === "healthy") healthy++;
|
|
335
|
-
else if (healthStatus === "degraded") degraded++;
|
|
336
|
-
else unreachable++;
|
|
337
|
-
totalContexts += w.assigned_contexts.length;
|
|
338
|
-
return {
|
|
339
|
-
id: w.id,
|
|
340
|
-
url: w.url,
|
|
341
|
-
health: healthStatus,
|
|
342
|
-
contexts: w.assigned_contexts,
|
|
343
|
-
current_task: status?.current_task ?? null,
|
|
344
|
-
activity: status?.activity ?? "idle",
|
|
345
|
-
uptime_seconds: health?.uptime_seconds ?? null,
|
|
346
|
-
capabilities: w.capabilities,
|
|
347
|
-
last_heartbeat: w.last_heartbeat
|
|
348
|
-
};
|
|
349
|
-
});
|
|
350
|
-
const assignedContexts = new Set(workers.flatMap((w) => w.assigned_contexts));
|
|
351
|
-
const unassigned = [...this.knownContexts].filter((c) => !assignedContexts.has(c));
|
|
352
|
-
return {
|
|
353
|
-
total_workers: workers.length,
|
|
354
|
-
healthy,
|
|
355
|
-
degraded,
|
|
356
|
-
unreachable,
|
|
357
|
-
total_contexts: totalContexts,
|
|
358
|
-
workers: workerSummaries,
|
|
359
|
-
unassigned_contexts: unassigned,
|
|
360
|
-
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
/** Get a quick status summary without refreshing health. */
|
|
364
|
-
getStatusSnapshot() {
|
|
365
|
-
const workers = this.primary.getWorkers();
|
|
366
|
-
let healthy = 0;
|
|
367
|
-
let degraded = 0;
|
|
368
|
-
let unreachable = 0;
|
|
369
|
-
let totalContexts = 0;
|
|
370
|
-
const workerSummaries = workers.map((w) => {
|
|
371
|
-
const health = this.latestHealth.get(w.id);
|
|
372
|
-
const status = this.latestStatus.get(w.id);
|
|
373
|
-
const healthStatus = health?.status ?? "unreachable";
|
|
374
|
-
if (healthStatus === "healthy") healthy++;
|
|
375
|
-
else if (healthStatus === "degraded") degraded++;
|
|
376
|
-
else unreachable++;
|
|
377
|
-
totalContexts += w.assigned_contexts.length;
|
|
378
|
-
return {
|
|
379
|
-
id: w.id,
|
|
380
|
-
url: w.url,
|
|
381
|
-
health: healthStatus,
|
|
382
|
-
contexts: w.assigned_contexts,
|
|
383
|
-
current_task: status?.current_task ?? null,
|
|
384
|
-
activity: status?.activity ?? "idle",
|
|
385
|
-
uptime_seconds: health?.uptime_seconds ?? null,
|
|
386
|
-
capabilities: w.capabilities,
|
|
387
|
-
last_heartbeat: w.last_heartbeat
|
|
388
|
-
};
|
|
389
|
-
});
|
|
390
|
-
const assignedContexts = new Set(workers.flatMap((w) => w.assigned_contexts));
|
|
391
|
-
const unassigned = [...this.knownContexts].filter((c) => !assignedContexts.has(c));
|
|
392
|
-
return {
|
|
393
|
-
total_workers: workers.length,
|
|
394
|
-
healthy,
|
|
395
|
-
degraded,
|
|
396
|
-
unreachable,
|
|
397
|
-
total_contexts: totalContexts,
|
|
398
|
-
workers: workerSummaries,
|
|
399
|
-
unassigned_contexts: unassigned,
|
|
400
|
-
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
// --- Worker Discovery ---
|
|
404
|
-
/**
|
|
405
|
-
* Scan a list of candidate URLs for workers. Returns the URLs
|
|
406
|
-
* that responded to a health probe. Useful for auto-detecting
|
|
407
|
-
* workers on a local network.
|
|
408
|
-
*/
|
|
409
|
-
async discoverWorkers(candidateUrls) {
|
|
410
|
-
const found = [];
|
|
411
|
-
const probes = candidateUrls.map(async (url) => {
|
|
412
|
-
const baseUrl = url.replace(/\/+$/, "");
|
|
413
|
-
try {
|
|
414
|
-
const controller = new AbortController();
|
|
415
|
-
const timeout = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
|
|
416
|
-
const resp = await fetch(`${baseUrl}${WORKER_ROUTES.health}`, {
|
|
417
|
-
signal: controller.signal
|
|
418
|
-
});
|
|
419
|
-
clearTimeout(timeout);
|
|
420
|
-
if (resp.ok) {
|
|
421
|
-
found.push(baseUrl);
|
|
422
|
-
}
|
|
423
|
-
} catch {
|
|
424
|
-
}
|
|
425
|
-
});
|
|
426
|
-
await Promise.all(probes);
|
|
427
|
-
return found;
|
|
428
|
-
}
|
|
429
|
-
/**
|
|
430
|
-
* Scan a subnet-style range for workers. Generates URLs for
|
|
431
|
-
* a base IP with ports and probes them.
|
|
432
|
-
* Example: scanSubnet("192.168.1", [10, 11, 12], 3100)
|
|
433
|
-
*/
|
|
434
|
-
async scanSubnet(baseIp, hostIds, port) {
|
|
435
|
-
const urls = hostIds.map((id) => `http://${baseIp}.${id}:${port}`);
|
|
436
|
-
return this.discoverWorkers(urls);
|
|
437
|
-
}
|
|
438
|
-
/** Track a context name so it appears in unassigned lists. */
|
|
439
|
-
registerContext(contextName) {
|
|
440
|
-
this.knownContexts.add(contextName);
|
|
441
|
-
}
|
|
442
|
-
/** Start health polling (delegates to PrimaryClient). */
|
|
443
|
-
startHealthPolling(intervalMs) {
|
|
444
|
-
this.primary.startHealthPolling(intervalMs);
|
|
445
|
-
}
|
|
446
|
-
/** Stop health polling. */
|
|
447
|
-
stopHealthPolling() {
|
|
448
|
-
this.primary.stopHealthPolling();
|
|
449
|
-
}
|
|
450
|
-
// --- Internal ---
|
|
451
|
-
async probeWorker(baseUrl) {
|
|
452
|
-
try {
|
|
453
|
-
const controller = new AbortController();
|
|
454
|
-
const timeout = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
|
|
455
|
-
const resp = await fetch(`${baseUrl}${WORKER_ROUTES.health}`, {
|
|
456
|
-
signal: controller.signal
|
|
457
|
-
});
|
|
458
|
-
clearTimeout(timeout);
|
|
459
|
-
if (resp.ok) {
|
|
460
|
-
const health = await resp.json();
|
|
461
|
-
return {
|
|
462
|
-
max_contexts: health.assigned_contexts.length + 4,
|
|
463
|
-
// assume capacity
|
|
464
|
-
has_ollama: health.ollama_ok,
|
|
465
|
-
has_memory_daemon: health.memory_daemon_ok,
|
|
466
|
-
available_models: []
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
} catch {
|
|
470
|
-
}
|
|
471
|
-
return {
|
|
472
|
-
max_contexts: 4,
|
|
473
|
-
has_ollama: false,
|
|
474
|
-
has_memory_daemon: false,
|
|
475
|
-
available_models: []
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
};
|
|
479
|
-
|
|
480
|
-
// packages/runtime/src/fleet/memory-sync.ts
|
|
481
|
-
function l3ToSync(entry) {
|
|
482
|
-
return {
|
|
483
|
-
id: entry.id,
|
|
484
|
-
source_episode_id: entry.source_episode_id,
|
|
485
|
-
context_name: entry.context_name,
|
|
486
|
-
content: entry.content,
|
|
487
|
-
promoted_at: entry.promoted_at,
|
|
488
|
-
access_count: entry.access_count,
|
|
489
|
-
connection_density: entry.connection_density,
|
|
490
|
-
updated_at: entry.promoted_at
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
function episodeToSync(ep) {
|
|
494
|
-
return {
|
|
495
|
-
id: ep.id,
|
|
496
|
-
timestamp: ep.timestamp,
|
|
497
|
-
context_name: ep.context_name,
|
|
498
|
-
role: ep.role,
|
|
499
|
-
content: ep.content
|
|
500
|
-
};
|
|
501
|
-
}
|
|
502
|
-
var WorkerMemorySync = class {
|
|
503
|
-
workerId;
|
|
504
|
-
primaryUrl;
|
|
505
|
-
memory;
|
|
506
|
-
syncTimer = null;
|
|
507
|
-
syncIntervalMs;
|
|
508
|
-
knownL2Ids = /* @__PURE__ */ new Set();
|
|
509
|
-
knownL3Ids = /* @__PURE__ */ new Map();
|
|
510
|
-
// id -> updated_at
|
|
511
|
-
constructor(opts) {
|
|
512
|
-
this.workerId = opts.workerId;
|
|
513
|
-
this.primaryUrl = opts.primaryUrl;
|
|
514
|
-
this.memory = opts.memory;
|
|
515
|
-
this.syncIntervalMs = opts.syncIntervalMs ?? DEFAULT_SYNC_INTERVAL_MS;
|
|
516
|
-
opts.server.onSyncPush((req) => this.handlePush(req));
|
|
517
|
-
}
|
|
518
|
-
/** Start periodic sync (Worker -> Primary). */
|
|
519
|
-
start(contextNames) {
|
|
520
|
-
this.stop();
|
|
521
|
-
this.syncTimer = setInterval(() => {
|
|
522
|
-
this.pullTowardsPrimary(contextNames()).catch((err) => {
|
|
523
|
-
console.warn("[sync] Pull cycle failed:", err.message);
|
|
524
|
-
});
|
|
525
|
-
}, this.syncIntervalMs);
|
|
526
|
-
}
|
|
527
|
-
/** Stop periodic sync. */
|
|
528
|
-
stop() {
|
|
529
|
-
if (this.syncTimer) {
|
|
530
|
-
clearInterval(this.syncTimer);
|
|
531
|
-
this.syncTimer = null;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
/**
|
|
535
|
-
* Pull L3 knowledge from all assigned contexts and send to Primary.
|
|
536
|
-
* Called periodically by the sync timer.
|
|
537
|
-
*/
|
|
538
|
-
async pullTowardsPrimary(contextNames) {
|
|
539
|
-
for (const contextName of contextNames) {
|
|
540
|
-
try {
|
|
541
|
-
const entries = await this.memory.getL3Knowledge(contextName);
|
|
542
|
-
if (entries.length === 0) continue;
|
|
543
|
-
const syncEntries = entries.map(l3ToSync);
|
|
544
|
-
const req = {
|
|
545
|
-
worker_id: this.workerId,
|
|
546
|
-
context_name: contextName,
|
|
547
|
-
entries: syncEntries
|
|
548
|
-
};
|
|
549
|
-
const controller = new AbortController();
|
|
550
|
-
const timeout = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
|
|
551
|
-
const resp = await fetch(`${this.primaryUrl}${PRIMARY_ROUTES.syncPull}`, {
|
|
552
|
-
method: "POST",
|
|
553
|
-
headers: { "Content-Type": "application/json" },
|
|
554
|
-
body: JSON.stringify(req),
|
|
555
|
-
signal: controller.signal
|
|
556
|
-
});
|
|
557
|
-
clearTimeout(timeout);
|
|
558
|
-
if (resp.ok) {
|
|
559
|
-
const result = await resp.json();
|
|
560
|
-
if (result.accepted > 0) {
|
|
561
|
-
console.log(`[sync] Pushed ${result.accepted} L3 entries from "${contextName}" to Primary`);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
} catch {
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
/**
|
|
569
|
-
* Handle an incoming push of Global context updates from the Primary.
|
|
570
|
-
* L3: last-write-wins. L2: append-only.
|
|
571
|
-
*/
|
|
572
|
-
async handlePush(req) {
|
|
573
|
-
let l3Accepted = 0;
|
|
574
|
-
let l2Appended = 0;
|
|
575
|
-
for (const entry of req.entries) {
|
|
576
|
-
const existing = this.knownL3Ids.get(entry.id);
|
|
577
|
-
if (existing && existing >= entry.updated_at) {
|
|
578
|
-
continue;
|
|
579
|
-
}
|
|
580
|
-
try {
|
|
581
|
-
await this.memory.storeEpisode({
|
|
582
|
-
context_name: entry.context_name,
|
|
583
|
-
role: "system",
|
|
584
|
-
content: entry.content
|
|
585
|
-
});
|
|
586
|
-
this.knownL3Ids.set(entry.id, entry.updated_at);
|
|
587
|
-
l3Accepted++;
|
|
588
|
-
} catch {
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
for (const episode of req.episodes) {
|
|
592
|
-
if (this.knownL2Ids.has(episode.id)) {
|
|
593
|
-
continue;
|
|
594
|
-
}
|
|
595
|
-
try {
|
|
596
|
-
await this.memory.storeEpisode({
|
|
597
|
-
context_name: episode.context_name,
|
|
598
|
-
role: episode.role,
|
|
599
|
-
content: episode.content
|
|
600
|
-
});
|
|
601
|
-
this.knownL2Ids.add(episode.id);
|
|
602
|
-
l2Appended++;
|
|
603
|
-
} catch {
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
if (l3Accepted > 0 || l2Appended > 0) {
|
|
607
|
-
console.log(`[sync] Received push: ${l3Accepted} L3 accepted, ${l2Appended} L2 appended`);
|
|
608
|
-
}
|
|
609
|
-
return { l3_accepted: l3Accepted, l2_appended: l2Appended };
|
|
610
|
-
}
|
|
611
|
-
getSyncIntervalMs() {
|
|
612
|
-
return this.syncIntervalMs;
|
|
613
|
-
}
|
|
614
|
-
};
|
|
615
|
-
var PrimaryMemorySync = class {
|
|
616
|
-
primary;
|
|
617
|
-
memory;
|
|
618
|
-
syncIntervalMs;
|
|
619
|
-
pushTimer = null;
|
|
620
|
-
l3Store = /* @__PURE__ */ new Map();
|
|
621
|
-
// id -> latest entry
|
|
622
|
-
lastPushAt = (/* @__PURE__ */ new Date(0)).toISOString();
|
|
623
|
-
constructor(opts) {
|
|
624
|
-
this.primary = opts.primary;
|
|
625
|
-
this.memory = opts.memory;
|
|
626
|
-
this.syncIntervalMs = opts.syncIntervalMs ?? DEFAULT_SYNC_INTERVAL_MS;
|
|
627
|
-
this.primary.onSyncPull((req) => this.handlePull(req));
|
|
628
|
-
}
|
|
629
|
-
/**
|
|
630
|
-
* Handle an incoming pull — Worker sending L3 knowledge to Primary.
|
|
631
|
-
* Uses last-write-wins for conflict resolution.
|
|
632
|
-
*/
|
|
633
|
-
async handlePull(req) {
|
|
634
|
-
let accepted = 0;
|
|
635
|
-
let rejected = 0;
|
|
636
|
-
for (const entry of req.entries) {
|
|
637
|
-
const existing = this.l3Store.get(entry.id);
|
|
638
|
-
if (existing && existing.updated_at >= entry.updated_at) {
|
|
639
|
-
rejected++;
|
|
640
|
-
continue;
|
|
641
|
-
}
|
|
642
|
-
this.l3Store.set(entry.id, entry);
|
|
643
|
-
try {
|
|
644
|
-
await this.memory.storeEpisode({
|
|
645
|
-
context_name: entry.context_name,
|
|
646
|
-
role: "system",
|
|
647
|
-
content: entry.content
|
|
648
|
-
});
|
|
649
|
-
} catch {
|
|
650
|
-
}
|
|
651
|
-
accepted++;
|
|
652
|
-
}
|
|
653
|
-
if (accepted > 0) {
|
|
654
|
-
console.log(
|
|
655
|
-
`[sync] Accepted ${accepted} L3 entries from worker "${req.worker_id}" context "${req.context_name}"`
|
|
656
|
-
);
|
|
657
|
-
}
|
|
658
|
-
return { accepted, rejected };
|
|
659
|
-
}
|
|
660
|
-
/**
|
|
661
|
-
* Push Global context updates to all workers.
|
|
662
|
-
* Fetches current Global L3 knowledge and recent L2 episodes,
|
|
663
|
-
* then pushes to every registered worker.
|
|
664
|
-
*/
|
|
665
|
-
async pushGlobalToAll() {
|
|
666
|
-
let l3Entries = [];
|
|
667
|
-
try {
|
|
668
|
-
const knowledge = await this.memory.getL3Knowledge("global");
|
|
669
|
-
l3Entries = knowledge.map(l3ToSync);
|
|
670
|
-
} catch {
|
|
671
|
-
}
|
|
672
|
-
let l2Episodes = [];
|
|
673
|
-
try {
|
|
674
|
-
const episodes = await this.memory.getContext("global");
|
|
675
|
-
l2Episodes = episodes.filter((ep) => ep.timestamp > this.lastPushAt).map(episodeToSync);
|
|
676
|
-
} catch {
|
|
677
|
-
}
|
|
678
|
-
if (l3Entries.length === 0 && l2Episodes.length === 0) {
|
|
679
|
-
return /* @__PURE__ */ new Map();
|
|
680
|
-
}
|
|
681
|
-
const payload = {
|
|
682
|
-
entries: l3Entries,
|
|
683
|
-
episodes: l2Episodes
|
|
684
|
-
};
|
|
685
|
-
this.lastPushAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
686
|
-
const results = await this.primary.pushSyncToAll(payload);
|
|
687
|
-
let totalL3 = 0;
|
|
688
|
-
let totalL2 = 0;
|
|
689
|
-
for (const resp of results.values()) {
|
|
690
|
-
if (resp) {
|
|
691
|
-
totalL3 += resp.l3_accepted;
|
|
692
|
-
totalL2 += resp.l2_appended;
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
if (totalL3 > 0 || totalL2 > 0) {
|
|
696
|
-
console.log(`[sync] Pushed Global updates: ${totalL3} L3, ${totalL2} L2 across ${results.size} workers`);
|
|
697
|
-
}
|
|
698
|
-
return results;
|
|
699
|
-
}
|
|
700
|
-
/** Start periodic Global push to all workers. */
|
|
701
|
-
startPushLoop() {
|
|
702
|
-
this.stopPushLoop();
|
|
703
|
-
this.pushTimer = setInterval(() => {
|
|
704
|
-
this.pushGlobalToAll().catch((err) => {
|
|
705
|
-
console.warn("[sync] Push cycle failed:", err.message);
|
|
706
|
-
});
|
|
707
|
-
}, this.syncIntervalMs);
|
|
708
|
-
}
|
|
709
|
-
/** Stop periodic push. */
|
|
710
|
-
stopPushLoop() {
|
|
711
|
-
if (this.pushTimer) {
|
|
712
|
-
clearInterval(this.pushTimer);
|
|
713
|
-
this.pushTimer = null;
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
/** Get all synced L3 entries the Primary has received. */
|
|
717
|
-
getSyncedEntries() {
|
|
718
|
-
return Array.from(this.l3Store.values());
|
|
719
|
-
}
|
|
720
|
-
getSyncIntervalMs() {
|
|
721
|
-
return this.syncIntervalMs;
|
|
722
|
-
}
|
|
723
|
-
};
|
|
724
|
-
|
|
725
|
-
export {
|
|
726
|
-
PrimaryClient,
|
|
727
|
-
FleetManager,
|
|
728
|
-
WorkerMemorySync,
|
|
729
|
-
PrimaryMemorySync
|
|
730
|
-
};
|
|
731
|
-
//# sourceMappingURL=chunk-NVJ424TB.js.map
|