@naisys/hub 3.0.0-beta.45 → 3.0.0-beta.46
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.
|
@@ -85,6 +85,29 @@ export function createHubAgentService(naisysServer, { hubDb }, logService, heart
|
|
|
85
85
|
const { bestHostId, payload, onResponse } = args;
|
|
86
86
|
const startUserId = payload.startUserId;
|
|
87
87
|
const runtimeApiKey = await issueRuntimeApiKey(startUserId);
|
|
88
|
+
// Fetch before run_session.create — a query failure here would otherwise
|
|
89
|
+
// leak the placeholder. Hashes ship so the host can skip already-on-disk files.
|
|
90
|
+
const startupAttachmentRows = await hubDb.user_startup_attachments.findMany({
|
|
91
|
+
where: { user_id: startUserId },
|
|
92
|
+
orderBy: { path: "asc" },
|
|
93
|
+
include: {
|
|
94
|
+
attachment: {
|
|
95
|
+
select: {
|
|
96
|
+
public_id: true,
|
|
97
|
+
filename: true,
|
|
98
|
+
file_size: true,
|
|
99
|
+
file_hash: true,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
const startupAttachments = startupAttachmentRows.map((r) => ({
|
|
105
|
+
publicId: r.attachment.public_id,
|
|
106
|
+
filename: r.attachment.filename,
|
|
107
|
+
fileSize: r.attachment.file_size,
|
|
108
|
+
fileHash: r.attachment.file_hash,
|
|
109
|
+
path: r.path,
|
|
110
|
+
}));
|
|
88
111
|
const lastRun = await hubDb.run_session.findFirst({
|
|
89
112
|
select: { run_id: true },
|
|
90
113
|
orderBy: { run_id: "desc" },
|
|
@@ -116,7 +139,13 @@ export function createHubAgentService(naisysServer, { hubDb }, logService, heart
|
|
|
116
139
|
logService.error(`[Hub:Agents] Failed to roll back run_session row for run ${runId} (${reason}): ${err}`);
|
|
117
140
|
}
|
|
118
141
|
}
|
|
119
|
-
const sent = naisysServer.sendMessage(bestHostId, HubEvents.AGENT_START, {
|
|
142
|
+
const sent = naisysServer.sendMessage(bestHostId, HubEvents.AGENT_START, {
|
|
143
|
+
...payload,
|
|
144
|
+
runtimeApiKey,
|
|
145
|
+
runId,
|
|
146
|
+
sessionId,
|
|
147
|
+
...(startupAttachments.length > 0 ? { startupAttachments } : {}),
|
|
148
|
+
}, async (response) => {
|
|
120
149
|
if (response.success && response.modelName) {
|
|
121
150
|
const modelName = response.modelName;
|
|
122
151
|
try {
|
|
@@ -24,10 +24,25 @@ export function createHubHeartbeatService(naisysServer, { hubDb }, logService, r
|
|
|
24
24
|
const activeUserIds = [
|
|
25
25
|
...new Set(parsed.activeSessions.map((s) => s.userId)),
|
|
26
26
|
];
|
|
27
|
-
//
|
|
27
|
+
// Memory before DB: supervisor online badges read this map live, so a
|
|
28
|
+
// slow SQLite write shouldn't make heartbeats look stale.
|
|
28
29
|
hostActiveAgents.set(hostId, activeUserIds);
|
|
30
|
+
const now = new Date().toISOString();
|
|
31
|
+
const sessionMap = new Map();
|
|
32
|
+
for (const session of parsed.activeSessions) {
|
|
33
|
+
const subagentId = session.subagentId ?? 0;
|
|
34
|
+
sessionMap.set(sessionKey(session.userId, subagentId), {
|
|
35
|
+
userId: session.userId,
|
|
36
|
+
subagentId: subagentId === 0 ? null : subagentId,
|
|
37
|
+
runId: session.runId,
|
|
38
|
+
sessionId: session.sessionId,
|
|
39
|
+
lastActive: now,
|
|
40
|
+
paused: session.paused,
|
|
41
|
+
state: session.state,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
hostActiveSessions.set(hostId, sessionMap);
|
|
29
45
|
try {
|
|
30
|
-
const now = new Date().toISOString();
|
|
31
46
|
// Update host last_active
|
|
32
47
|
await hubDb.hosts.updateMany({
|
|
33
48
|
where: { id: hostId },
|
|
@@ -43,19 +58,6 @@ export function createHubHeartbeatService(naisysServer, { hubDb }, logService, r
|
|
|
43
58
|
// Bump run_session.last_active for each active session so the run-online
|
|
44
59
|
// badge stays lit even during quiet periods with no log writes. The
|
|
45
60
|
// aggregate SESSION_HEARTBEAT broadcast runs on its own interval below.
|
|
46
|
-
const sessionMap = new Map();
|
|
47
|
-
for (const session of parsed.activeSessions) {
|
|
48
|
-
const subagentId = session.subagentId ?? 0;
|
|
49
|
-
sessionMap.set(sessionKey(session.userId, subagentId), {
|
|
50
|
-
userId: session.userId,
|
|
51
|
-
subagentId: subagentId === 0 ? null : subagentId,
|
|
52
|
-
runId: session.runId,
|
|
53
|
-
sessionId: session.sessionId,
|
|
54
|
-
lastActive: now,
|
|
55
|
-
paused: session.paused,
|
|
56
|
-
state: session.state,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
61
|
if (parsed.activeSessions.length > 0) {
|
|
60
62
|
await hubDb.run_session.updateMany({
|
|
61
63
|
where: {
|
|
@@ -69,7 +71,6 @@ export function createHubHeartbeatService(naisysServer, { hubDb }, logService, r
|
|
|
69
71
|
data: { last_active: now },
|
|
70
72
|
});
|
|
71
73
|
}
|
|
72
|
-
hostActiveSessions.set(hostId, sessionMap);
|
|
73
74
|
// Self-heal: register each plaintext with the redactor (idempotent),
|
|
74
75
|
// then mint + push a fresh key if the hash is missing or mismatched.
|
|
75
76
|
// Old plaintexts accumulate per user so leaks during the rotate
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { HUB_HEARTBEAT_INTERVAL_MS, HubEvents } from "@naisys/hub-protocol";
|
|
2
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
3
|
+
import { createHubHeartbeatService } from "./hubHeartbeatService.js";
|
|
4
|
+
function createDeferred() {
|
|
5
|
+
let resolve;
|
|
6
|
+
const promise = new Promise((r) => {
|
|
7
|
+
resolve = r;
|
|
8
|
+
});
|
|
9
|
+
return { promise, resolve };
|
|
10
|
+
}
|
|
11
|
+
function createServerHarness() {
|
|
12
|
+
const handlers = new Map();
|
|
13
|
+
const server = {
|
|
14
|
+
registerEvent: vi.fn((event, handler) => {
|
|
15
|
+
handlers.set(event, handler);
|
|
16
|
+
}),
|
|
17
|
+
broadcastToAll: vi.fn(),
|
|
18
|
+
broadcastToSupervisors: vi.fn(),
|
|
19
|
+
sendMessage: vi.fn(() => true),
|
|
20
|
+
};
|
|
21
|
+
return { server, handlers };
|
|
22
|
+
}
|
|
23
|
+
function createLogger() {
|
|
24
|
+
return {
|
|
25
|
+
log: vi.fn(),
|
|
26
|
+
error: vi.fn(),
|
|
27
|
+
disableConsole: vi.fn(),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
describe("hubHeartbeatService", () => {
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
vi.useRealTimers();
|
|
33
|
+
vi.restoreAllMocks();
|
|
34
|
+
});
|
|
35
|
+
test("publishes session heartbeats from memory before heartbeat DB writes finish", async () => {
|
|
36
|
+
vi.useFakeTimers();
|
|
37
|
+
vi.setSystemTime(new Date("2026-05-09T12:00:00Z"));
|
|
38
|
+
const { server, handlers } = createServerHarness();
|
|
39
|
+
const hostUpdate = createDeferred();
|
|
40
|
+
const hubDb = {
|
|
41
|
+
hosts: {
|
|
42
|
+
updateMany: vi.fn(() => hostUpdate.promise),
|
|
43
|
+
},
|
|
44
|
+
user_notifications: {
|
|
45
|
+
updateMany: vi.fn(() => Promise.resolve({ count: 1 })),
|
|
46
|
+
},
|
|
47
|
+
run_session: {
|
|
48
|
+
updateMany: vi.fn(() => Promise.resolve({ count: 1 })),
|
|
49
|
+
},
|
|
50
|
+
users: {
|
|
51
|
+
findMany: vi.fn(() => Promise.resolve([])),
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
const redactionService = {
|
|
55
|
+
registerRuntimeApiKey: vi.fn(),
|
|
56
|
+
};
|
|
57
|
+
const runtimeKeyService = {
|
|
58
|
+
issueRuntimeApiKey: vi.fn(),
|
|
59
|
+
};
|
|
60
|
+
const service = createHubHeartbeatService(server, { hubDb }, createLogger(), redactionService, runtimeKeyService);
|
|
61
|
+
try {
|
|
62
|
+
const heartbeatHandler = handlers.get(HubEvents.HEARTBEAT);
|
|
63
|
+
if (!heartbeatHandler) {
|
|
64
|
+
throw new Error("HEARTBEAT handler was not registered");
|
|
65
|
+
}
|
|
66
|
+
const handlerPromise = Promise.resolve(heartbeatHandler(42, {
|
|
67
|
+
activeSessions: [
|
|
68
|
+
{
|
|
69
|
+
userId: 1,
|
|
70
|
+
runId: 7,
|
|
71
|
+
sessionId: 2,
|
|
72
|
+
paused: true,
|
|
73
|
+
state: "Waiting",
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
}));
|
|
77
|
+
expect(hubDb.hosts.updateMany).toHaveBeenCalled();
|
|
78
|
+
await vi.advanceTimersByTimeAsync(HUB_HEARTBEAT_INTERVAL_MS);
|
|
79
|
+
expect(server.broadcastToSupervisors).toHaveBeenCalledWith(HubEvents.SESSION_HEARTBEAT, {
|
|
80
|
+
updates: [
|
|
81
|
+
{
|
|
82
|
+
userId: 1,
|
|
83
|
+
runId: 7,
|
|
84
|
+
subagentId: undefined,
|
|
85
|
+
sessionId: 2,
|
|
86
|
+
lastActive: "2026-05-09T12:00:00.000Z",
|
|
87
|
+
paused: true,
|
|
88
|
+
state: "Waiting",
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
});
|
|
92
|
+
hostUpdate.resolve({ count: 1 });
|
|
93
|
+
await handlerPromise;
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
service.cleanup();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
//# sourceMappingURL=hubHeartbeatService.test.js.map
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naisys/hub",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.46",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@naisys/hub",
|
|
9
|
-
"version": "3.0.0-beta.
|
|
9
|
+
"version": "3.0.0-beta.46",
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@naisys/common": "3.0.0-beta.
|
|
12
|
-
"@naisys/common-node": "3.0.0-beta.
|
|
13
|
-
"@naisys/hub-database": "3.0.0-beta.
|
|
14
|
-
"@naisys/hub-protocol": "3.0.0-beta.
|
|
11
|
+
"@naisys/common": "3.0.0-beta.46",
|
|
12
|
+
"@naisys/common-node": "3.0.0-beta.46",
|
|
13
|
+
"@naisys/hub-database": "3.0.0-beta.46",
|
|
14
|
+
"@naisys/hub-protocol": "3.0.0-beta.46",
|
|
15
15
|
"commander": "^14.0.3",
|
|
16
16
|
"dotenv": "^17.3.1",
|
|
17
17
|
"fastify": "^5.8.2",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"node": ">=22.0.0"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
|
-
"@naisys/supervisor": "3.0.0-beta.
|
|
27
|
+
"@naisys/supervisor": "3.0.0-beta.46"
|
|
28
28
|
},
|
|
29
29
|
"peerDependenciesMeta": {
|
|
30
30
|
"@naisys/supervisor": {
|
|
@@ -189,32 +189,32 @@
|
|
|
189
189
|
"license": "MIT"
|
|
190
190
|
},
|
|
191
191
|
"node_modules/@naisys/common": {
|
|
192
|
-
"version": "3.0.0-beta.
|
|
193
|
-
"resolved": "https://registry.npmjs.org/@naisys/common/-/common-3.0.0-beta.
|
|
194
|
-
"integrity": "sha512-
|
|
192
|
+
"version": "3.0.0-beta.46",
|
|
193
|
+
"resolved": "https://registry.npmjs.org/@naisys/common/-/common-3.0.0-beta.46.tgz",
|
|
194
|
+
"integrity": "sha512-C08UtZtQLp0/3u1cAAfWRtFssphQgrkKk63HhhE/ntCZFWUlfhgTB4xtBabwJZitp+hqjLJbC3A7yRIGv6gj5w==",
|
|
195
195
|
"dependencies": {
|
|
196
196
|
"semver": "^7.7.4",
|
|
197
197
|
"zod": "^4.3.6"
|
|
198
198
|
}
|
|
199
199
|
},
|
|
200
200
|
"node_modules/@naisys/common-node": {
|
|
201
|
-
"version": "3.0.0-beta.
|
|
202
|
-
"resolved": "https://registry.npmjs.org/@naisys/common-node/-/common-node-3.0.0-beta.
|
|
203
|
-
"integrity": "sha512-
|
|
201
|
+
"version": "3.0.0-beta.46",
|
|
202
|
+
"resolved": "https://registry.npmjs.org/@naisys/common-node/-/common-node-3.0.0-beta.46.tgz",
|
|
203
|
+
"integrity": "sha512-MV+HYrb8lLWrvCkF5Of4qvi0989Y1w9T3CkQrdm/zajkXtwN685uBUg7ty++rcvNnVeZWcDptl46Kb5PSaKjCw==",
|
|
204
204
|
"dependencies": {
|
|
205
|
-
"@naisys/common": "3.0.0-beta.
|
|
205
|
+
"@naisys/common": "3.0.0-beta.46",
|
|
206
206
|
"better-sqlite3": "^12.6.2",
|
|
207
207
|
"js-yaml": "^4.1.1",
|
|
208
208
|
"pino": "^10.3.1"
|
|
209
209
|
}
|
|
210
210
|
},
|
|
211
211
|
"node_modules/@naisys/hub-database": {
|
|
212
|
-
"version": "3.0.0-beta.
|
|
213
|
-
"resolved": "https://registry.npmjs.org/@naisys/hub-database/-/hub-database-3.0.0-beta.
|
|
214
|
-
"integrity": "sha512-
|
|
212
|
+
"version": "3.0.0-beta.46",
|
|
213
|
+
"resolved": "https://registry.npmjs.org/@naisys/hub-database/-/hub-database-3.0.0-beta.46.tgz",
|
|
214
|
+
"integrity": "sha512-o2PIeGv9Rt2M0LSKIhDA9MZLdYzhswn8uIL/AuP4CFd8zwq564p3a7g4wT34cjgOplyHLEFdaj1KWOcPbQVeTw==",
|
|
215
215
|
"dependencies": {
|
|
216
|
-
"@naisys/common": "3.0.0-beta.
|
|
217
|
-
"@naisys/common-node": "3.0.0-beta.
|
|
216
|
+
"@naisys/common": "3.0.0-beta.46",
|
|
217
|
+
"@naisys/common-node": "3.0.0-beta.46",
|
|
218
218
|
"@prisma/adapter-better-sqlite3": "^7.5.0",
|
|
219
219
|
"@prisma/client": "^7.5.0",
|
|
220
220
|
"better-sqlite3": "^12.6.2",
|
|
@@ -222,11 +222,11 @@
|
|
|
222
222
|
}
|
|
223
223
|
},
|
|
224
224
|
"node_modules/@naisys/hub-protocol": {
|
|
225
|
-
"version": "3.0.0-beta.
|
|
226
|
-
"resolved": "https://registry.npmjs.org/@naisys/hub-protocol/-/hub-protocol-3.0.0-beta.
|
|
227
|
-
"integrity": "sha512-
|
|
225
|
+
"version": "3.0.0-beta.46",
|
|
226
|
+
"resolved": "https://registry.npmjs.org/@naisys/hub-protocol/-/hub-protocol-3.0.0-beta.46.tgz",
|
|
227
|
+
"integrity": "sha512-XcKK22yHxsjzY9uAN8ljpRaMHFnVzepWez0tCtkAc936XR5X+0keybKsvAirvxONBAfOYKJ72jXV5+16qqfllA==",
|
|
228
228
|
"dependencies": {
|
|
229
|
-
"@naisys/common": "3.0.0-beta.
|
|
229
|
+
"@naisys/common": "3.0.0-beta.46",
|
|
230
230
|
"zod": "^4.3.6"
|
|
231
231
|
}
|
|
232
232
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naisys/hub",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.46",
|
|
4
4
|
"description": "NAISYS Hub - Adds persistence and multi-instance coordination to NAISYS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/naisysHub.js",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"!dist/**/*.d.ts.map"
|
|
33
33
|
],
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@naisys/supervisor": "3.0.0-beta.
|
|
35
|
+
"@naisys/supervisor": "3.0.0-beta.46"
|
|
36
36
|
},
|
|
37
37
|
"peerDependenciesMeta": {
|
|
38
38
|
"@naisys/supervisor": {
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@naisys/common": "3.0.0-beta.
|
|
44
|
-
"@naisys/common-node": "3.0.0-beta.
|
|
45
|
-
"@naisys/hub-database": "3.0.0-beta.
|
|
46
|
-
"@naisys/hub-protocol": "3.0.0-beta.
|
|
43
|
+
"@naisys/common": "3.0.0-beta.46",
|
|
44
|
+
"@naisys/common-node": "3.0.0-beta.46",
|
|
45
|
+
"@naisys/hub-database": "3.0.0-beta.46",
|
|
46
|
+
"@naisys/hub-protocol": "3.0.0-beta.46",
|
|
47
47
|
"commander": "^14.0.3",
|
|
48
48
|
"dotenv": "^17.3.1",
|
|
49
49
|
"fastify": "^5.8.2",
|