hzl-core 2.10.0 → 3.1.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/dist/__tests__/migrations/v5-upgrade.test.d.ts +2 -0
- package/dist/__tests__/migrations/v5-upgrade.test.d.ts.map +1 -0
- package/dist/__tests__/migrations/v5-upgrade.test.js +56 -0
- package/dist/__tests__/migrations/v5-upgrade.test.js.map +1 -0
- package/dist/db/migrations/index.d.ts.map +1 -1
- package/dist/db/migrations/index.js +5 -0
- package/dist/db/migrations/index.js.map +1 -1
- package/dist/db/migrations/v5.d.ts +2 -0
- package/dist/db/migrations/v5.d.ts.map +1 -0
- package/dist/db/migrations/v5.js +4 -0
- package/dist/db/migrations/v5.js.map +1 -0
- package/dist/db/schema.d.ts +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +1 -0
- package/dist/db/schema.js.map +1 -1
- package/dist/db/test-seed.d.ts +22 -0
- package/dist/db/test-seed.d.ts.map +1 -0
- package/dist/db/test-seed.js +72 -0
- package/dist/db/test-seed.js.map +1 -0
- package/dist/events/store.d.ts +8 -0
- package/dist/events/store.d.ts.map +1 -1
- package/dist/events/store.js +19 -0
- package/dist/events/store.js.map +1 -1
- package/dist/events/store.test.js +54 -0
- package/dist/events/store.test.js.map +1 -1
- package/dist/events/types.d.ts +3 -1
- package/dist/events/types.d.ts.map +1 -1
- package/dist/events/types.js +4 -0
- package/dist/events/types.js.map +1 -1
- package/dist/events/types.test.js +31 -0
- package/dist/events/types.test.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/projections/tasks-current.d.ts.map +1 -1
- package/dist/projections/tasks-current.js +3 -3
- package/dist/projections/tasks-current.js.map +1 -1
- package/dist/projections/tasks-current.test.js +18 -0
- package/dist/projections/tasks-current.test.js.map +1 -1
- package/dist/services/gateway-client.d.ts +56 -0
- package/dist/services/gateway-client.d.ts.map +1 -0
- package/dist/services/gateway-client.js +413 -0
- package/dist/services/gateway-client.js.map +1 -0
- package/dist/services/stats-service.d.ts +51 -0
- package/dist/services/stats-service.d.ts.map +1 -0
- package/dist/services/stats-service.js +183 -0
- package/dist/services/stats-service.js.map +1 -0
- package/dist/services/stats-service.test.d.ts +2 -0
- package/dist/services/stats-service.test.d.ts.map +1 -0
- package/dist/services/stats-service.test.js +196 -0
- package/dist/services/stats-service.test.js.map +1 -0
- package/dist/services/task-service.d.ts +3 -9
- package/dist/services/task-service.d.ts.map +1 -1
- package/dist/services/task-service.js +17 -34
- package/dist/services/task-service.js.map +1 -1
- package/dist/services/task-service.test.js +61 -32
- package/dist/services/task-service.test.js.map +1 -1
- package/dist/services/workflow-service.d.ts.map +1 -1
- package/dist/services/workflow-service.js +1 -14
- package/dist/services/workflow-service.js.map +1 -1
- package/dist/utils/duration.d.ts +3 -0
- package/dist/utils/duration.d.ts.map +1 -0
- package/dist/utils/duration.js +21 -0
- package/dist/utils/duration.js.map +1 -0
- package/dist/utils/duration.test.d.ts +2 -0
- package/dist/utils/duration.test.d.ts.map +1 -0
- package/dist/utils/duration.test.js +50 -0
- package/dist/utils/duration.test.js.map +1 -0
- package/dist/utils/json.d.ts +6 -0
- package/dist/utils/json.d.ts.map +1 -0
- package/dist/utils/json.js +27 -0
- package/dist/utils/json.js.map +1 -0
- package/dist/utils/json.test.d.ts +2 -0
- package/dist/utils/json.test.d.ts.map +1 -0
- package/dist/utils/json.test.js +30 -0
- package/dist/utils/json.test.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
// packages/hzl-core/src/services/gateway-client.ts
|
|
2
|
+
import { generateKeyPairSync, sign, createPrivateKey, createHash } from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
const RPC_TIMEOUT_MS = 30_000;
|
|
6
|
+
const MAX_RECONNECT_DELAY_MS = 30_000;
|
|
7
|
+
const INITIAL_RECONNECT_DELAY_MS = 1_000;
|
|
8
|
+
const PAIRING_RETRY_DELAY_MS = 5_000;
|
|
9
|
+
const DEVICE_IDENTITY_FILE = 'gateway-device.json';
|
|
10
|
+
const PROTOCOL_VERSION = 3;
|
|
11
|
+
const ED25519_SPKI_PREFIX_LEN = 12; // ASN.1 header length for Ed25519 SPKI DER
|
|
12
|
+
function base64UrlEncode(buf) {
|
|
13
|
+
return buf.toString('base64').replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/g, '');
|
|
14
|
+
}
|
|
15
|
+
export class GatewayClient {
|
|
16
|
+
url;
|
|
17
|
+
token;
|
|
18
|
+
configDir;
|
|
19
|
+
ws = null;
|
|
20
|
+
_status = 'disconnected';
|
|
21
|
+
deviceIdentity = null;
|
|
22
|
+
pendingCalls = new Map();
|
|
23
|
+
nextId = 1;
|
|
24
|
+
reconnectTimer;
|
|
25
|
+
reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
|
|
26
|
+
connectPromise = null;
|
|
27
|
+
disposed = false;
|
|
28
|
+
pairingMessageShown = false;
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.url = options.url;
|
|
31
|
+
this.token = options.token;
|
|
32
|
+
this.configDir = options.configDir;
|
|
33
|
+
this.deviceIdentity = this.loadDeviceIdentity();
|
|
34
|
+
}
|
|
35
|
+
getStatus() {
|
|
36
|
+
return this._status;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Update gateway configuration. Disconnects and reconnects if URL or token changed.
|
|
40
|
+
*/
|
|
41
|
+
configure(url, token) {
|
|
42
|
+
const changed = url !== this.url || token !== this.token;
|
|
43
|
+
this.url = url;
|
|
44
|
+
this.token = token;
|
|
45
|
+
if (changed && this._status === 'connected') {
|
|
46
|
+
this.disconnect();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Send an RPC call to the gateway. Lazily connects on first call.
|
|
51
|
+
*/
|
|
52
|
+
async call(method, params) {
|
|
53
|
+
await this.ensureConnected();
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const id = String(this.nextId++);
|
|
56
|
+
const timer = setTimeout(() => {
|
|
57
|
+
this.pendingCalls.delete(id);
|
|
58
|
+
reject(new Error(`Gateway RPC timeout: ${method}`));
|
|
59
|
+
}, RPC_TIMEOUT_MS);
|
|
60
|
+
this.pendingCalls.set(id, { resolve, reject, timer });
|
|
61
|
+
const frame = { type: 'req', id, method };
|
|
62
|
+
if (params !== undefined) {
|
|
63
|
+
frame.params = params;
|
|
64
|
+
}
|
|
65
|
+
this.ws.send(JSON.stringify(frame));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Disconnect from the gateway and clean up resources.
|
|
70
|
+
*/
|
|
71
|
+
dispose() {
|
|
72
|
+
this.disposed = true;
|
|
73
|
+
this.disconnect();
|
|
74
|
+
}
|
|
75
|
+
disconnect() {
|
|
76
|
+
if (this.reconnectTimer) {
|
|
77
|
+
clearTimeout(this.reconnectTimer);
|
|
78
|
+
this.reconnectTimer = undefined;
|
|
79
|
+
}
|
|
80
|
+
if (this.ws) {
|
|
81
|
+
this.ws.onopen = null;
|
|
82
|
+
this.ws.onclose = null;
|
|
83
|
+
this.ws.onerror = null;
|
|
84
|
+
this.ws.onmessage = null;
|
|
85
|
+
if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
|
|
86
|
+
this.ws.close();
|
|
87
|
+
}
|
|
88
|
+
this.ws = null;
|
|
89
|
+
}
|
|
90
|
+
// Reject all pending calls
|
|
91
|
+
for (const call of this.pendingCalls.values()) {
|
|
92
|
+
clearTimeout(call.timer);
|
|
93
|
+
call.reject(new Error('Gateway disconnected'));
|
|
94
|
+
}
|
|
95
|
+
this.pendingCalls.clear();
|
|
96
|
+
this._status = 'disconnected';
|
|
97
|
+
this.connectPromise = null;
|
|
98
|
+
}
|
|
99
|
+
async ensureConnected() {
|
|
100
|
+
if (this._status === 'connected' && this.ws?.readyState === WebSocket.OPEN) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (this.connectPromise) {
|
|
104
|
+
return this.connectPromise;
|
|
105
|
+
}
|
|
106
|
+
this.connectPromise = this.connect();
|
|
107
|
+
try {
|
|
108
|
+
await this.connectPromise;
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
this.connectPromise = null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
connect() {
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
this._status = 'connecting';
|
|
117
|
+
const ws = new WebSocket(this.url);
|
|
118
|
+
this.ws = ws;
|
|
119
|
+
let handshakeComplete = false;
|
|
120
|
+
let challengeNonce = null;
|
|
121
|
+
ws.onopen = () => {
|
|
122
|
+
this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
|
|
123
|
+
};
|
|
124
|
+
ws.onmessage = (event) => {
|
|
125
|
+
const raw = String(event.data);
|
|
126
|
+
let msg;
|
|
127
|
+
try {
|
|
128
|
+
msg = JSON.parse(raw);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// Handshake phase
|
|
134
|
+
if (!handshakeComplete) {
|
|
135
|
+
// Step 1: Receive connect.challenge
|
|
136
|
+
// Gateway sends: { type: "event", event: "connect.challenge", payload: { nonce, ts } }
|
|
137
|
+
const isChallenge = (msg.event === 'connect.challenge' && msg.payload) ||
|
|
138
|
+
(msg.method === 'connect.challenge' && (msg.params ?? msg.payload));
|
|
139
|
+
const challengeData = msg.payload ?? msg.params;
|
|
140
|
+
if (isChallenge && challengeData) {
|
|
141
|
+
challengeNonce = typeof challengeData.nonce === 'string' ? challengeData.nonce : '';
|
|
142
|
+
this.sendConnectFrame(ws, challengeNonce);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Step 2: Receive hello-ok response
|
|
146
|
+
// Gateway sends: { type: "res", id: "…", ok: true, payload: { type: "hello-ok", ... } }
|
|
147
|
+
const isHelloOk = (msg.type === 'res' && msg.ok === true && msg.payload) ||
|
|
148
|
+
(msg.type === 'res' && !msg.error && challengeNonce);
|
|
149
|
+
if (isHelloOk) {
|
|
150
|
+
handshakeComplete = true;
|
|
151
|
+
this._status = 'connected';
|
|
152
|
+
if (this.pairingMessageShown) {
|
|
153
|
+
console.log(' Device approved — gateway connected.');
|
|
154
|
+
console.log('');
|
|
155
|
+
this.pairingMessageShown = false;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
console.log(`Gateway connected (${this.url})`);
|
|
159
|
+
}
|
|
160
|
+
// Persist device token if provided
|
|
161
|
+
// Token is at payload.auth.deviceToken
|
|
162
|
+
const payload = msg.payload;
|
|
163
|
+
const auth = payload?.auth;
|
|
164
|
+
const deviceToken = auth?.deviceToken;
|
|
165
|
+
if (typeof deviceToken === 'string' && this.deviceIdentity) {
|
|
166
|
+
this.deviceIdentity.deviceToken = deviceToken;
|
|
167
|
+
this.saveDeviceIdentity(this.deviceIdentity);
|
|
168
|
+
}
|
|
169
|
+
resolve();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
// Handshake error
|
|
173
|
+
if (msg.type === 'res' && (msg.error || msg.ok === false)) {
|
|
174
|
+
const errorMsg = msg.error?.message ?? 'unknown error';
|
|
175
|
+
const details = msg.error?.details;
|
|
176
|
+
const rawCode = details?.code ?? msg.error?.code;
|
|
177
|
+
const code = typeof rawCode === 'string' ? rawCode : '';
|
|
178
|
+
if (code === 'PAIRING_REQUIRED') {
|
|
179
|
+
if (!this.pairingMessageShown) {
|
|
180
|
+
const rawReqId = details?.requestId;
|
|
181
|
+
const requestId = typeof rawReqId === 'string' ? rawReqId : '';
|
|
182
|
+
console.log('');
|
|
183
|
+
console.log(' Device pairing required');
|
|
184
|
+
console.log('');
|
|
185
|
+
console.log(' This device must be approved on your OpenClaw gateway.');
|
|
186
|
+
console.log(' Run on your gateway host:');
|
|
187
|
+
console.log('');
|
|
188
|
+
console.log(` openclaw devices approve ${requestId}`);
|
|
189
|
+
console.log('');
|
|
190
|
+
console.log(' Waiting for approval...');
|
|
191
|
+
this.pairingMessageShown = true;
|
|
192
|
+
}
|
|
193
|
+
// Retry every 5s — user may approve at any time
|
|
194
|
+
this._status = 'disconnected';
|
|
195
|
+
this.reconnectDelay = PAIRING_RETRY_DELAY_MS;
|
|
196
|
+
reject(new Error(`Gateway handshake failed: ${errorMsg}`));
|
|
197
|
+
if (!this.disposed) {
|
|
198
|
+
this.scheduleReconnect();
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const err = new Error(`Gateway handshake failed: ${errorMsg}`);
|
|
203
|
+
this._status = 'disconnected';
|
|
204
|
+
reject(err);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
// Normal RPC response
|
|
210
|
+
if (msg.type === 'res' && msg.id) {
|
|
211
|
+
const call = this.pendingCalls.get(msg.id);
|
|
212
|
+
if (call) {
|
|
213
|
+
this.pendingCalls.delete(msg.id);
|
|
214
|
+
clearTimeout(call.timer);
|
|
215
|
+
if (msg.error) {
|
|
216
|
+
call.reject(new Error(msg.error.message));
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
// Result may be in msg.result or msg.payload
|
|
220
|
+
call.resolve(msg.result ?? msg.payload);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
ws.onclose = () => {
|
|
226
|
+
const wasConnected = handshakeComplete;
|
|
227
|
+
this._status = 'disconnected';
|
|
228
|
+
if (!handshakeComplete) {
|
|
229
|
+
reject(new Error('Gateway connection closed during handshake'));
|
|
230
|
+
}
|
|
231
|
+
// Reject pending calls
|
|
232
|
+
for (const call of this.pendingCalls.values()) {
|
|
233
|
+
clearTimeout(call.timer);
|
|
234
|
+
call.reject(new Error('Gateway connection closed'));
|
|
235
|
+
}
|
|
236
|
+
this.pendingCalls.clear();
|
|
237
|
+
// Auto-reconnect if we were connected and not disposed
|
|
238
|
+
if (wasConnected && !this.disposed) {
|
|
239
|
+
this.scheduleReconnect();
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
ws.onerror = () => {
|
|
243
|
+
if (!handshakeComplete) {
|
|
244
|
+
this._status = 'disconnected';
|
|
245
|
+
reject(new Error(`Gateway connection failed: ${this.url}`));
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
sendConnectFrame(ws, nonce) {
|
|
251
|
+
const identity = this.getOrCreateDeviceIdentity();
|
|
252
|
+
const connectId = String(this.nextId++);
|
|
253
|
+
const signedAt = Date.now();
|
|
254
|
+
const clientId = 'gateway-client';
|
|
255
|
+
const clientMode = 'backend';
|
|
256
|
+
const role = 'operator';
|
|
257
|
+
const scopes = ['operator.read', 'operator.write', 'operator.admin'];
|
|
258
|
+
const token = identity.deviceToken ?? this.token ?? '';
|
|
259
|
+
const platform = process.platform;
|
|
260
|
+
// Build v3 auth payload: v3|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce|platform|deviceFamily
|
|
261
|
+
const authPayload = [
|
|
262
|
+
'v3',
|
|
263
|
+
identity.id,
|
|
264
|
+
clientId,
|
|
265
|
+
clientMode,
|
|
266
|
+
role,
|
|
267
|
+
scopes.join(','),
|
|
268
|
+
String(signedAt),
|
|
269
|
+
token,
|
|
270
|
+
nonce,
|
|
271
|
+
platform.toLowerCase(),
|
|
272
|
+
'', // deviceFamily (empty for server)
|
|
273
|
+
].join('|');
|
|
274
|
+
const signature = this.signPayload(authPayload, identity.privateKeyPkcs8);
|
|
275
|
+
const frame = {
|
|
276
|
+
type: 'req',
|
|
277
|
+
id: connectId,
|
|
278
|
+
method: 'connect',
|
|
279
|
+
params: {
|
|
280
|
+
minProtocol: PROTOCOL_VERSION,
|
|
281
|
+
maxProtocol: PROTOCOL_VERSION,
|
|
282
|
+
client: {
|
|
283
|
+
id: clientId,
|
|
284
|
+
version: '1.0.0',
|
|
285
|
+
platform,
|
|
286
|
+
mode: clientMode,
|
|
287
|
+
},
|
|
288
|
+
role,
|
|
289
|
+
scopes,
|
|
290
|
+
caps: [],
|
|
291
|
+
commands: [],
|
|
292
|
+
permissions: {},
|
|
293
|
+
auth: { token: identity.deviceToken ?? this.token },
|
|
294
|
+
locale: 'en-US',
|
|
295
|
+
userAgent: 'hzl-dashboard/1.0.0',
|
|
296
|
+
device: {
|
|
297
|
+
id: identity.id,
|
|
298
|
+
publicKey: identity.publicKey,
|
|
299
|
+
signature,
|
|
300
|
+
signedAt,
|
|
301
|
+
nonce,
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
ws.send(JSON.stringify(frame));
|
|
306
|
+
}
|
|
307
|
+
signPayload(payload, privateKeyPkcs8Base64) {
|
|
308
|
+
const privateKeyDer = Buffer.from(privateKeyPkcs8Base64, 'base64');
|
|
309
|
+
const key = createPrivateKey({
|
|
310
|
+
key: privateKeyDer,
|
|
311
|
+
format: 'der',
|
|
312
|
+
type: 'pkcs8',
|
|
313
|
+
});
|
|
314
|
+
const sig = sign(null, Buffer.from(payload, 'utf8'), key);
|
|
315
|
+
return base64UrlEncode(sig);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Derive device ID as SHA256 hex of the raw 32-byte Ed25519 public key.
|
|
319
|
+
* publicKeyBase64Url is the raw 32-byte key in base64url encoding.
|
|
320
|
+
*/
|
|
321
|
+
deriveDeviceId(publicKeyBase64Url) {
|
|
322
|
+
// Decode base64url → raw 32 bytes → SHA256 hex
|
|
323
|
+
const normalized = publicKeyBase64Url.replaceAll('-', '+').replaceAll('_', '/');
|
|
324
|
+
const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4);
|
|
325
|
+
const raw = Buffer.from(padded, 'base64');
|
|
326
|
+
return createHash('sha256').update(raw).digest('hex');
|
|
327
|
+
}
|
|
328
|
+
getOrCreateDeviceIdentity() {
|
|
329
|
+
if (this.deviceIdentity) {
|
|
330
|
+
let changed = false;
|
|
331
|
+
// Migrate old SPKI base64 public keys to raw base64url
|
|
332
|
+
// SPKI Ed25519 keys start with 'MCowBQ' (the ASN.1 header)
|
|
333
|
+
if (this.deviceIdentity.publicKey.startsWith('MCowBQ')) {
|
|
334
|
+
const spkiDer = Buffer.from(this.deviceIdentity.publicKey, 'base64');
|
|
335
|
+
const rawKey = spkiDer.subarray(ED25519_SPKI_PREFIX_LEN);
|
|
336
|
+
this.deviceIdentity.publicKey = base64UrlEncode(rawKey);
|
|
337
|
+
changed = true;
|
|
338
|
+
}
|
|
339
|
+
// Validate stored ID matches derived fingerprint
|
|
340
|
+
const expectedId = this.deriveDeviceId(this.deviceIdentity.publicKey);
|
|
341
|
+
if (this.deviceIdentity.id !== expectedId) {
|
|
342
|
+
this.deviceIdentity.id = expectedId;
|
|
343
|
+
changed = true;
|
|
344
|
+
}
|
|
345
|
+
if (changed) {
|
|
346
|
+
this.saveDeviceIdentity(this.deviceIdentity);
|
|
347
|
+
}
|
|
348
|
+
return this.deviceIdentity;
|
|
349
|
+
}
|
|
350
|
+
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
351
|
+
// Extract raw 32-byte public key from SPKI DER (strip 12-byte ASN.1 header)
|
|
352
|
+
const spkiDer = publicKey.export({ type: 'spki', format: 'der' });
|
|
353
|
+
const rawPub = spkiDer.subarray(ED25519_SPKI_PREFIX_LEN);
|
|
354
|
+
const pubBase64Url = base64UrlEncode(rawPub);
|
|
355
|
+
const identity = {
|
|
356
|
+
id: this.deriveDeviceId(pubBase64Url),
|
|
357
|
+
publicKey: pubBase64Url,
|
|
358
|
+
privateKeyPkcs8: privateKey.export({ type: 'pkcs8', format: 'der' }).toString('base64'),
|
|
359
|
+
};
|
|
360
|
+
this.deviceIdentity = identity;
|
|
361
|
+
this.saveDeviceIdentity(identity);
|
|
362
|
+
return identity;
|
|
363
|
+
}
|
|
364
|
+
loadDeviceIdentity() {
|
|
365
|
+
const filePath = path.join(this.configDir, DEVICE_IDENTITY_FILE);
|
|
366
|
+
try {
|
|
367
|
+
if (!fs.existsSync(filePath))
|
|
368
|
+
return null;
|
|
369
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
370
|
+
const id = data.id;
|
|
371
|
+
const publicKey = data.publicKey;
|
|
372
|
+
// Support both old ('privateKey') and new ('privateKeyPkcs8') field names
|
|
373
|
+
const privateKeyPkcs8 = (data.privateKeyPkcs8 ?? data.privateKey);
|
|
374
|
+
if (id && publicKey && privateKeyPkcs8) {
|
|
375
|
+
return { id, publicKey, privateKeyPkcs8, deviceToken: data.deviceToken };
|
|
376
|
+
}
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
saveDeviceIdentity(identity) {
|
|
384
|
+
try {
|
|
385
|
+
if (!fs.existsSync(this.configDir)) {
|
|
386
|
+
fs.mkdirSync(this.configDir, { recursive: true });
|
|
387
|
+
}
|
|
388
|
+
const filePath = path.join(this.configDir, DEVICE_IDENTITY_FILE);
|
|
389
|
+
const tempPath = `${filePath}.tmp.${process.pid}`;
|
|
390
|
+
fs.writeFileSync(tempPath, JSON.stringify(identity, null, 2) + '\n');
|
|
391
|
+
fs.renameSync(tempPath, filePath);
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
// Non-fatal: device identity will be regenerated next time
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
scheduleReconnect() {
|
|
398
|
+
if (this.disposed || this.reconnectTimer)
|
|
399
|
+
return;
|
|
400
|
+
this.reconnectTimer = setTimeout(() => {
|
|
401
|
+
this.reconnectTimer = undefined;
|
|
402
|
+
if (!this.disposed) {
|
|
403
|
+
this.ensureConnected().catch(() => {
|
|
404
|
+
// Reconnect failed, schedule another attempt
|
|
405
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
|
|
406
|
+
this.scheduleReconnect();
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}, this.reconnectDelay);
|
|
410
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
//# sourceMappingURL=gateway-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gateway-client.js","sourceRoot":"","sources":["../../src/services/gateway-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type Database from 'libsql';
|
|
2
|
+
import type { TaskService } from './task-service.js';
|
|
3
|
+
export interface StatsQueryOptions {
|
|
4
|
+
project?: string;
|
|
5
|
+
windowMinutes?: number;
|
|
6
|
+
windowLabel?: string;
|
|
7
|
+
asOf?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface QueueStats {
|
|
10
|
+
backlog: number;
|
|
11
|
+
ready: number;
|
|
12
|
+
in_progress: number;
|
|
13
|
+
blocked: number;
|
|
14
|
+
done: number;
|
|
15
|
+
archived: number;
|
|
16
|
+
available: number;
|
|
17
|
+
stale: number;
|
|
18
|
+
expired_leases: number;
|
|
19
|
+
}
|
|
20
|
+
export interface CompletionStats {
|
|
21
|
+
total: number;
|
|
22
|
+
by_agent: Record<string, number>;
|
|
23
|
+
}
|
|
24
|
+
export interface ExecutionTimeStats {
|
|
25
|
+
count: number;
|
|
26
|
+
mean: number | null;
|
|
27
|
+
min: number | null;
|
|
28
|
+
max: number | null;
|
|
29
|
+
excluded_without_start: number;
|
|
30
|
+
}
|
|
31
|
+
export interface StatsSnapshot {
|
|
32
|
+
window: string;
|
|
33
|
+
generated_at: string;
|
|
34
|
+
projects: string[];
|
|
35
|
+
queue: QueueStats;
|
|
36
|
+
completions: CompletionStats;
|
|
37
|
+
execution_time_ms: ExecutionTimeStats;
|
|
38
|
+
}
|
|
39
|
+
export declare class StatsService {
|
|
40
|
+
private cacheDb;
|
|
41
|
+
private eventsDb;
|
|
42
|
+
private taskService;
|
|
43
|
+
constructor(cacheDb: Database.Database, eventsDb: Database.Database, taskService: Pick<TaskService, 'getAvailableTasks' | 'getStaleTasks'>);
|
|
44
|
+
getStats(options?: StatsQueryOptions): StatsSnapshot;
|
|
45
|
+
private getProjects;
|
|
46
|
+
private getTrackedTasks;
|
|
47
|
+
private getQueueStats;
|
|
48
|
+
private getExpiredLeaseCount;
|
|
49
|
+
private getHistoricalStats;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=stats-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stats-service.d.ts","sourceRoot":"","sources":["../../src/services/stats-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,QAAQ,CAAC;AACnC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAuBrD,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,sBAAsB,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,EAAE,UAAU,CAAC;IAClB,WAAW,EAAE,eAAe,CAAC;IAC7B,iBAAiB,EAAE,kBAAkB,CAAC;CACvC;AAED,qBAAa,YAAY;IAErB,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,WAAW;gBAFX,OAAO,EAAE,QAAQ,CAAC,QAAQ,EAC1B,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAC3B,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,mBAAmB,GAAG,eAAe,CAAC;IAG/E,QAAQ,CAAC,OAAO,GAAE,iBAAsB,GAAG,aAAa;IAyBxD,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,aAAa;IAoCrB,OAAO,CAAC,oBAAoB;IAoB5B,OAAO,CAAC,kBAAkB;CAuG3B"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
const DEFAULT_STALE_THRESHOLD_MINUTES = 10;
|
|
2
|
+
const DEFAULT_WINDOW_MINUTES = 24 * 60;
|
|
3
|
+
const TASK_CHUNK_SIZE = 500;
|
|
4
|
+
export class StatsService {
|
|
5
|
+
cacheDb;
|
|
6
|
+
eventsDb;
|
|
7
|
+
taskService;
|
|
8
|
+
constructor(cacheDb, eventsDb, taskService) {
|
|
9
|
+
this.cacheDb = cacheDb;
|
|
10
|
+
this.eventsDb = eventsDb;
|
|
11
|
+
this.taskService = taskService;
|
|
12
|
+
}
|
|
13
|
+
getStats(options = {}) {
|
|
14
|
+
const generatedAt = options.asOf ?? new Date().toISOString();
|
|
15
|
+
const windowMinutes = options.windowMinutes ?? DEFAULT_WINDOW_MINUTES;
|
|
16
|
+
const windowLabel = options.windowLabel ?? (windowMinutes === 24 * 60 ? '24h' : `${windowMinutes}m`);
|
|
17
|
+
const windowStart = new Date(new Date(generatedAt).getTime() - windowMinutes * 60_000).toISOString();
|
|
18
|
+
const trackedTasks = this.getTrackedTasks(options.project);
|
|
19
|
+
const historical = this.getHistoricalStats({
|
|
20
|
+
trackedTasks,
|
|
21
|
+
windowStart,
|
|
22
|
+
windowEnd: generatedAt,
|
|
23
|
+
});
|
|
24
|
+
return {
|
|
25
|
+
window: windowLabel,
|
|
26
|
+
generated_at: generatedAt,
|
|
27
|
+
projects: this.getProjects(),
|
|
28
|
+
queue: this.getQueueStats({ project: options.project, asOf: generatedAt }),
|
|
29
|
+
completions: historical.completions,
|
|
30
|
+
execution_time_ms: historical.execution_time_ms,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
getProjects() {
|
|
34
|
+
const rows = this.cacheDb
|
|
35
|
+
.prepare('SELECT name FROM projects ORDER BY name')
|
|
36
|
+
.all();
|
|
37
|
+
return rows.map((row) => row.name);
|
|
38
|
+
}
|
|
39
|
+
getTrackedTasks(project) {
|
|
40
|
+
const rows = (project
|
|
41
|
+
? this.cacheDb
|
|
42
|
+
.prepare('SELECT task_id, project, agent FROM tasks_current WHERE project = ?')
|
|
43
|
+
.all(project)
|
|
44
|
+
: this.cacheDb.prepare('SELECT task_id, project, agent FROM tasks_current').all());
|
|
45
|
+
return new Map(rows.map((row) => [row.task_id, row]));
|
|
46
|
+
}
|
|
47
|
+
getQueueStats(options) {
|
|
48
|
+
const params = [];
|
|
49
|
+
const projectWhere = options.project ? 'WHERE project = ?' : '';
|
|
50
|
+
if (options.project) {
|
|
51
|
+
params.push(options.project);
|
|
52
|
+
}
|
|
53
|
+
const rows = this.cacheDb.prepare(`
|
|
54
|
+
SELECT status, COUNT(*) as count
|
|
55
|
+
FROM tasks_current
|
|
56
|
+
${projectWhere}
|
|
57
|
+
GROUP BY status
|
|
58
|
+
`).all(...params);
|
|
59
|
+
const queue = {
|
|
60
|
+
backlog: 0,
|
|
61
|
+
ready: 0,
|
|
62
|
+
in_progress: 0,
|
|
63
|
+
blocked: 0,
|
|
64
|
+
done: 0,
|
|
65
|
+
archived: 0,
|
|
66
|
+
available: this.taskService.getAvailableTasks({ project: options.project }).length,
|
|
67
|
+
stale: this.taskService.getStaleTasks({
|
|
68
|
+
project: options.project,
|
|
69
|
+
thresholdMinutes: DEFAULT_STALE_THRESHOLD_MINUTES,
|
|
70
|
+
}).size,
|
|
71
|
+
expired_leases: this.getExpiredLeaseCount(options),
|
|
72
|
+
};
|
|
73
|
+
for (const row of rows) {
|
|
74
|
+
queue[row.status] = row.count;
|
|
75
|
+
}
|
|
76
|
+
return queue;
|
|
77
|
+
}
|
|
78
|
+
getExpiredLeaseCount(options) {
|
|
79
|
+
const params = [options.asOf];
|
|
80
|
+
let projectClause = '';
|
|
81
|
+
if (options.project) {
|
|
82
|
+
projectClause = 'AND project = ?';
|
|
83
|
+
params.push(options.project);
|
|
84
|
+
}
|
|
85
|
+
const row = this.cacheDb.prepare(`
|
|
86
|
+
SELECT COUNT(*) as count
|
|
87
|
+
FROM tasks_current
|
|
88
|
+
WHERE status = 'in_progress'
|
|
89
|
+
AND lease_until IS NOT NULL
|
|
90
|
+
AND lease_until < ?
|
|
91
|
+
${projectClause}
|
|
92
|
+
`).get(...params);
|
|
93
|
+
return row.count;
|
|
94
|
+
}
|
|
95
|
+
getHistoricalStats(options) {
|
|
96
|
+
const completions = {
|
|
97
|
+
total: 0,
|
|
98
|
+
by_agent: {},
|
|
99
|
+
};
|
|
100
|
+
const durations = [];
|
|
101
|
+
let excludedWithoutStart = 0;
|
|
102
|
+
if (options.trackedTasks.size === 0) {
|
|
103
|
+
return {
|
|
104
|
+
completions,
|
|
105
|
+
execution_time_ms: {
|
|
106
|
+
count: 0,
|
|
107
|
+
mean: null,
|
|
108
|
+
min: null,
|
|
109
|
+
max: null,
|
|
110
|
+
excluded_without_start: 0,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const taskIds = [...options.trackedTasks.keys()];
|
|
115
|
+
for (let index = 0; index < taskIds.length; index += TASK_CHUNK_SIZE) {
|
|
116
|
+
const chunk = taskIds.slice(index, index + TASK_CHUNK_SIZE);
|
|
117
|
+
const placeholders = chunk.map(() => '?').join(', ');
|
|
118
|
+
const rows = this.eventsDb.prepare(`
|
|
119
|
+
SELECT
|
|
120
|
+
e.task_id,
|
|
121
|
+
e.timestamp AS done_at,
|
|
122
|
+
(
|
|
123
|
+
SELECT start.timestamp
|
|
124
|
+
FROM events start
|
|
125
|
+
WHERE start.task_id = e.task_id
|
|
126
|
+
AND start.type = 'status_changed'
|
|
127
|
+
AND json_extract(start.data, '$.to') = 'in_progress'
|
|
128
|
+
AND start.id < e.id
|
|
129
|
+
ORDER BY start.id DESC
|
|
130
|
+
LIMIT 1
|
|
131
|
+
) AS started_at
|
|
132
|
+
FROM events e
|
|
133
|
+
WHERE e.type = 'status_changed'
|
|
134
|
+
AND json_extract(e.data, '$.to') = 'done'
|
|
135
|
+
AND e.timestamp >= ?
|
|
136
|
+
AND e.timestamp <= ?
|
|
137
|
+
AND e.task_id IN (${placeholders})
|
|
138
|
+
ORDER BY e.id ASC
|
|
139
|
+
`).all(options.windowStart, options.windowEnd, ...chunk);
|
|
140
|
+
for (const row of rows) {
|
|
141
|
+
const task = options.trackedTasks.get(row.task_id);
|
|
142
|
+
if (!task) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
completions.total += 1;
|
|
146
|
+
if (task.agent) {
|
|
147
|
+
completions.by_agent[task.agent] = (completions.by_agent[task.agent] ?? 0) + 1;
|
|
148
|
+
}
|
|
149
|
+
if (!row.started_at) {
|
|
150
|
+
excludedWithoutStart += 1;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const duration = new Date(row.done_at).getTime() - new Date(row.started_at).getTime();
|
|
154
|
+
if (Number.isFinite(duration) && duration >= 0) {
|
|
155
|
+
durations.push(duration);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
excludedWithoutStart += 1;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const executionTimeMs = durations.length === 0
|
|
163
|
+
? {
|
|
164
|
+
count: 0,
|
|
165
|
+
mean: null,
|
|
166
|
+
min: null,
|
|
167
|
+
max: null,
|
|
168
|
+
excluded_without_start: excludedWithoutStart,
|
|
169
|
+
}
|
|
170
|
+
: {
|
|
171
|
+
count: durations.length,
|
|
172
|
+
mean: durations.reduce((sum, duration) => sum + duration, 0) / durations.length,
|
|
173
|
+
min: durations.reduce((m, d) => (d < m ? d : m), Infinity),
|
|
174
|
+
max: durations.reduce((m, d) => (d > m ? d : m), -Infinity),
|
|
175
|
+
excluded_without_start: excludedWithoutStart,
|
|
176
|
+
};
|
|
177
|
+
return {
|
|
178
|
+
completions,
|
|
179
|
+
execution_time_ms: executionTimeMs,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=stats-service.js.map
|