@evomap/evolver 1.29.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 (52) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +290 -0
  3. package/README.zh-CN.md +236 -0
  4. package/SKILL.md +132 -0
  5. package/assets/gep/capsules.json +79 -0
  6. package/assets/gep/events.jsonl +7 -0
  7. package/assets/gep/genes.json +108 -0
  8. package/index.js +479 -0
  9. package/package.json +38 -0
  10. package/src/canary.js +13 -0
  11. package/src/evolve.js +1704 -0
  12. package/src/gep/a2a.js +173 -0
  13. package/src/gep/a2aProtocol.js +736 -0
  14. package/src/gep/analyzer.js +35 -0
  15. package/src/gep/assetCallLog.js +130 -0
  16. package/src/gep/assetStore.js +297 -0
  17. package/src/gep/assets.js +36 -0
  18. package/src/gep/bridge.js +71 -0
  19. package/src/gep/candidates.js +142 -0
  20. package/src/gep/contentHash.js +65 -0
  21. package/src/gep/deviceId.js +209 -0
  22. package/src/gep/envFingerprint.js +68 -0
  23. package/src/gep/hubReview.js +206 -0
  24. package/src/gep/hubSearch.js +237 -0
  25. package/src/gep/issueReporter.js +262 -0
  26. package/src/gep/llmReview.js +92 -0
  27. package/src/gep/memoryGraph.js +771 -0
  28. package/src/gep/memoryGraphAdapter.js +203 -0
  29. package/src/gep/mutation.js +186 -0
  30. package/src/gep/narrativeMemory.js +108 -0
  31. package/src/gep/paths.js +113 -0
  32. package/src/gep/personality.js +355 -0
  33. package/src/gep/prompt.js +566 -0
  34. package/src/gep/questionGenerator.js +212 -0
  35. package/src/gep/reflection.js +127 -0
  36. package/src/gep/sanitize.js +67 -0
  37. package/src/gep/selector.js +250 -0
  38. package/src/gep/signals.js +417 -0
  39. package/src/gep/skillDistiller.js +499 -0
  40. package/src/gep/solidify.js +1681 -0
  41. package/src/gep/strategy.js +126 -0
  42. package/src/gep/taskReceiver.js +528 -0
  43. package/src/gep/validationReport.js +55 -0
  44. package/src/ops/cleanup.js +80 -0
  45. package/src/ops/commentary.js +60 -0
  46. package/src/ops/health_check.js +106 -0
  47. package/src/ops/index.js +11 -0
  48. package/src/ops/innovation.js +67 -0
  49. package/src/ops/lifecycle.js +168 -0
  50. package/src/ops/self_repair.js +72 -0
  51. package/src/ops/skills_monitor.js +143 -0
  52. package/src/ops/trigger.js +33 -0
@@ -0,0 +1,736 @@
1
+ // GEP A2A Protocol - Standard message types and pluggable transport layer.
2
+ //
3
+ // Protocol messages:
4
+ // hello - capability advertisement and node discovery
5
+ // publish - broadcast an eligible asset (Capsule/Gene)
6
+ // fetch - request a specific asset by id or content hash
7
+ // report - send a ValidationReport for a received asset
8
+ // decision - accept/reject/quarantine decision on a received asset
9
+ // revoke - withdraw a previously published asset
10
+ //
11
+ // Transport interface:
12
+ // send(message, opts) - send a protocol message
13
+ // receive(opts) - receive pending messages
14
+ // list(opts) - list available message files/streams
15
+ //
16
+ // Default transport: FileTransport (reads/writes JSONL to a2a/ directory).
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const crypto = require('crypto');
21
+ const { getGepAssetsDir, getEvolverLogPath } = require('./paths');
22
+ const { computeAssetId } = require('./contentHash');
23
+ const { captureEnvFingerprint } = require('./envFingerprint');
24
+ const os = require('os');
25
+ const { getDeviceId } = require('./deviceId');
26
+
27
+ const PROTOCOL_NAME = 'gep-a2a';
28
+ const PROTOCOL_VERSION = '1.0.0';
29
+ const VALID_MESSAGE_TYPES = ['hello', 'publish', 'fetch', 'report', 'decision', 'revoke'];
30
+
31
+ const NODE_ID_RE = /^node_[a-f0-9]{12}$/;
32
+ const NODE_ID_DIR = path.join(os.homedir(), '.evomap');
33
+ const NODE_ID_FILE = path.join(NODE_ID_DIR, 'node_id');
34
+ const LOCAL_NODE_ID_FILE = path.resolve(__dirname, '..', '..', '.evomap_node_id');
35
+
36
+ let _cachedNodeId = null;
37
+
38
+ function _loadPersistedNodeId() {
39
+ try {
40
+ if (fs.existsSync(NODE_ID_FILE)) {
41
+ const id = fs.readFileSync(NODE_ID_FILE, 'utf8').trim();
42
+ if (id && NODE_ID_RE.test(id)) return id;
43
+ }
44
+ } catch {}
45
+ try {
46
+ if (fs.existsSync(LOCAL_NODE_ID_FILE)) {
47
+ const id = fs.readFileSync(LOCAL_NODE_ID_FILE, 'utf8').trim();
48
+ if (id && NODE_ID_RE.test(id)) return id;
49
+ }
50
+ } catch {}
51
+ return null;
52
+ }
53
+
54
+ function _persistNodeId(id) {
55
+ try {
56
+ if (!fs.existsSync(NODE_ID_DIR)) {
57
+ fs.mkdirSync(NODE_ID_DIR, { recursive: true, mode: 0o700 });
58
+ }
59
+ fs.writeFileSync(NODE_ID_FILE, id, { encoding: 'utf8', mode: 0o600 });
60
+ return;
61
+ } catch {}
62
+ try {
63
+ fs.writeFileSync(LOCAL_NODE_ID_FILE, id, { encoding: 'utf8', mode: 0o600 });
64
+ return;
65
+ } catch {}
66
+ }
67
+
68
+ function generateMessageId() {
69
+ return 'msg_' + Date.now() + '_' + crypto.randomBytes(4).toString('hex');
70
+ }
71
+
72
+ function getNodeId() {
73
+ if (_cachedNodeId) return _cachedNodeId;
74
+
75
+ if (process.env.A2A_NODE_ID) {
76
+ _cachedNodeId = String(process.env.A2A_NODE_ID);
77
+ return _cachedNodeId;
78
+ }
79
+
80
+ const persisted = _loadPersistedNodeId();
81
+ if (persisted) {
82
+ _cachedNodeId = persisted;
83
+ return _cachedNodeId;
84
+ }
85
+
86
+ console.warn('[a2aProtocol] A2A_NODE_ID is not set. Computing node ID from device fingerprint. ' +
87
+ 'This ID may change across machines or environments. ' +
88
+ 'Set A2A_NODE_ID after registering at https://evomap.ai to use a stable identity.');
89
+
90
+ const deviceId = getDeviceId();
91
+ const agentName = process.env.AGENT_NAME || 'default';
92
+ const raw = deviceId + '|' + agentName + '|' + process.cwd();
93
+ const computed = 'node_' + crypto.createHash('sha256').update(raw).digest('hex').slice(0, 12);
94
+
95
+ _persistNodeId(computed);
96
+ _cachedNodeId = computed;
97
+ return _cachedNodeId;
98
+ }
99
+
100
+ // --- Base message builder ---
101
+
102
+ function buildMessage(params) {
103
+ if (!params || typeof params !== 'object') {
104
+ throw new Error('buildMessage requires a params object');
105
+ }
106
+ var messageType = params.messageType;
107
+ var payload = params.payload;
108
+ var senderId = params.senderId;
109
+ if (!VALID_MESSAGE_TYPES.includes(messageType)) {
110
+ throw new Error('Invalid message type: ' + messageType + '. Valid: ' + VALID_MESSAGE_TYPES.join(', '));
111
+ }
112
+ return {
113
+ protocol: PROTOCOL_NAME,
114
+ protocol_version: PROTOCOL_VERSION,
115
+ message_type: messageType,
116
+ message_id: generateMessageId(),
117
+ sender_id: senderId || getNodeId(),
118
+ timestamp: new Date().toISOString(),
119
+ payload: payload || {},
120
+ };
121
+ }
122
+
123
+ // --- Typed message builders ---
124
+
125
+ function buildHello(opts) {
126
+ var o = opts || {};
127
+ return buildMessage({
128
+ messageType: 'hello',
129
+ senderId: o.nodeId,
130
+ payload: {
131
+ capabilities: o.capabilities || {},
132
+ gene_count: typeof o.geneCount === 'number' ? o.geneCount : null,
133
+ capsule_count: typeof o.capsuleCount === 'number' ? o.capsuleCount : null,
134
+ env_fingerprint: captureEnvFingerprint(),
135
+ },
136
+ });
137
+ }
138
+
139
+ function buildPublish(opts) {
140
+ var o = opts || {};
141
+ var asset = o.asset;
142
+ if (!asset || !asset.type || !asset.id) {
143
+ throw new Error('publish: asset must have type and id');
144
+ }
145
+ // Generate signature: HMAC-SHA256 of asset_id with node secret
146
+ var assetIdVal = asset.asset_id || computeAssetId(asset);
147
+ var nodeSecret = process.env.A2A_NODE_SECRET || getNodeId();
148
+ var signature = crypto.createHmac('sha256', nodeSecret).update(assetIdVal).digest('hex');
149
+ return buildMessage({
150
+ messageType: 'publish',
151
+ senderId: o.nodeId,
152
+ payload: {
153
+ asset_type: asset.type,
154
+ asset_id: assetIdVal,
155
+ local_id: asset.id,
156
+ asset: asset,
157
+ signature: signature,
158
+ },
159
+ });
160
+ }
161
+
162
+ // Build a bundle publish message containing Gene + Capsule (+ optional EvolutionEvent).
163
+ // Hub requires payload.assets = [Gene, Capsule] since bundle enforcement was added.
164
+ function buildPublishBundle(opts) {
165
+ var o = opts || {};
166
+ var gene = o.gene;
167
+ var capsule = o.capsule;
168
+ var event = o.event || null;
169
+ if (!gene || gene.type !== 'Gene' || !gene.id) {
170
+ throw new Error('publishBundle: gene must be a valid Gene with type and id');
171
+ }
172
+ if (!capsule || capsule.type !== 'Capsule' || !capsule.id) {
173
+ throw new Error('publishBundle: capsule must be a valid Capsule with type and id');
174
+ }
175
+ var geneAssetId = gene.asset_id || computeAssetId(gene);
176
+ var capsuleAssetId = capsule.asset_id || computeAssetId(capsule);
177
+ var nodeSecret = process.env.A2A_NODE_SECRET || getNodeId();
178
+ var signatureInput = [geneAssetId, capsuleAssetId].sort().join('|');
179
+ var signature = crypto.createHmac('sha256', nodeSecret).update(signatureInput).digest('hex');
180
+ if (o.modelName && typeof o.modelName === 'string') {
181
+ gene.model_name = o.modelName;
182
+ capsule.model_name = o.modelName;
183
+ }
184
+ var assets = [gene, capsule];
185
+ if (event && event.type === 'EvolutionEvent') {
186
+ if (o.modelName && typeof o.modelName === 'string') {
187
+ event.model_name = o.modelName;
188
+ }
189
+ assets.push(event);
190
+ }
191
+ var publishPayload = {
192
+ assets: assets,
193
+ signature: signature,
194
+ };
195
+ if (o.chainId && typeof o.chainId === 'string') {
196
+ publishPayload.chain_id = o.chainId;
197
+ }
198
+ return buildMessage({
199
+ messageType: 'publish',
200
+ senderId: o.nodeId,
201
+ payload: publishPayload,
202
+ });
203
+ }
204
+
205
+ function buildFetch(opts) {
206
+ var o = opts || {};
207
+ var fetchPayload = {
208
+ asset_type: o.assetType || null,
209
+ local_id: o.localId || null,
210
+ content_hash: o.contentHash || null,
211
+ };
212
+ if (Array.isArray(o.signals) && o.signals.length > 0) {
213
+ fetchPayload.signals = o.signals;
214
+ }
215
+ if (o.searchOnly === true) {
216
+ fetchPayload.search_only = true;
217
+ }
218
+ if (Array.isArray(o.assetIds) && o.assetIds.length > 0) {
219
+ fetchPayload.asset_ids = o.assetIds;
220
+ }
221
+ return buildMessage({
222
+ messageType: 'fetch',
223
+ senderId: o.nodeId,
224
+ payload: fetchPayload,
225
+ });
226
+ }
227
+
228
+ function buildReport(opts) {
229
+ var o = opts || {};
230
+ return buildMessage({
231
+ messageType: 'report',
232
+ senderId: o.nodeId,
233
+ payload: {
234
+ target_asset_id: o.assetId || null,
235
+ target_local_id: o.localId || null,
236
+ validation_report: o.validationReport || null,
237
+ },
238
+ });
239
+ }
240
+
241
+ function buildDecision(opts) {
242
+ var o = opts || {};
243
+ var validDecisions = ['accept', 'reject', 'quarantine'];
244
+ if (!validDecisions.includes(o.decision)) {
245
+ throw new Error('decision must be one of: ' + validDecisions.join(', '));
246
+ }
247
+ return buildMessage({
248
+ messageType: 'decision',
249
+ senderId: o.nodeId,
250
+ payload: {
251
+ target_asset_id: o.assetId || null,
252
+ target_local_id: o.localId || null,
253
+ decision: o.decision,
254
+ reason: o.reason || null,
255
+ },
256
+ });
257
+ }
258
+
259
+ function buildRevoke(opts) {
260
+ var o = opts || {};
261
+ return buildMessage({
262
+ messageType: 'revoke',
263
+ senderId: o.nodeId,
264
+ payload: {
265
+ target_asset_id: o.assetId || null,
266
+ target_local_id: o.localId || null,
267
+ reason: o.reason || null,
268
+ },
269
+ });
270
+ }
271
+
272
+ // --- Validation ---
273
+
274
+ function isValidProtocolMessage(msg) {
275
+ if (!msg || typeof msg !== 'object') return false;
276
+ if (msg.protocol !== PROTOCOL_NAME) return false;
277
+ if (!msg.message_type || !VALID_MESSAGE_TYPES.includes(msg.message_type)) return false;
278
+ if (!msg.message_id || typeof msg.message_id !== 'string') return false;
279
+ if (!msg.timestamp || typeof msg.timestamp !== 'string') return false;
280
+ return true;
281
+ }
282
+
283
+ // Try to extract a raw asset from either a protocol message or a plain asset object.
284
+ // This enables backward-compatible ingestion of both old-format and new-format payloads.
285
+ function unwrapAssetFromMessage(input) {
286
+ if (!input || typeof input !== 'object') return null;
287
+ // If it is a protocol message with a publish payload, extract the asset.
288
+ if (input.protocol === PROTOCOL_NAME && input.message_type === 'publish') {
289
+ var p = input.payload;
290
+ if (p && p.asset && typeof p.asset === 'object') return p.asset;
291
+ return null;
292
+ }
293
+ // If it is a plain asset (Gene/Capsule/EvolutionEvent), return as-is.
294
+ if (input.type === 'Gene' || input.type === 'Capsule' || input.type === 'EvolutionEvent') {
295
+ return input;
296
+ }
297
+ return null;
298
+ }
299
+
300
+ // --- File Transport ---
301
+
302
+ function ensureDir(dir) {
303
+ try {
304
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
305
+ } catch (e) {}
306
+ }
307
+
308
+ function defaultA2ADir() {
309
+ return process.env.A2A_DIR || path.join(getGepAssetsDir(), 'a2a');
310
+ }
311
+
312
+ function fileTransportSend(message, opts) {
313
+ var dir = (opts && opts.dir) || defaultA2ADir();
314
+ var subdir = path.join(dir, 'outbox');
315
+ ensureDir(subdir);
316
+ var filePath = path.join(subdir, message.message_type + '.jsonl');
317
+ fs.appendFileSync(filePath, JSON.stringify(message) + '\n', 'utf8');
318
+ return { ok: true, path: filePath };
319
+ }
320
+
321
+ function fileTransportReceive(opts) {
322
+ var dir = (opts && opts.dir) || defaultA2ADir();
323
+ var subdir = path.join(dir, 'inbox');
324
+ if (!fs.existsSync(subdir)) return [];
325
+ var files = fs.readdirSync(subdir).filter(function (f) { return f.endsWith('.jsonl'); });
326
+ var messages = [];
327
+ for (var fi = 0; fi < files.length; fi++) {
328
+ try {
329
+ var raw = fs.readFileSync(path.join(subdir, files[fi]), 'utf8');
330
+ var lines = raw.split('\n').map(function (l) { return l.trim(); }).filter(Boolean);
331
+ for (var li = 0; li < lines.length; li++) {
332
+ try {
333
+ var msg = JSON.parse(lines[li]);
334
+ if (msg && msg.protocol === PROTOCOL_NAME) messages.push(msg);
335
+ } catch (e) {}
336
+ }
337
+ } catch (e) {}
338
+ }
339
+ return messages;
340
+ }
341
+
342
+ function fileTransportList(opts) {
343
+ var dir = (opts && opts.dir) || defaultA2ADir();
344
+ var subdir = path.join(dir, 'outbox');
345
+ if (!fs.existsSync(subdir)) return [];
346
+ return fs.readdirSync(subdir).filter(function (f) { return f.endsWith('.jsonl'); });
347
+ }
348
+
349
+ // --- HTTP Transport (connects to evomap-hub) ---
350
+
351
+ function httpTransportSend(message, opts) {
352
+ var hubUrl = (opts && opts.hubUrl) || process.env.A2A_HUB_URL;
353
+ if (!hubUrl) return { ok: false, error: 'A2A_HUB_URL not set' };
354
+ var endpoint = hubUrl.replace(/\/+$/, '') + '/a2a/' + message.message_type;
355
+ var body = JSON.stringify(message);
356
+ return fetch(endpoint, {
357
+ method: 'POST',
358
+ headers: buildHubHeaders(),
359
+ body: body,
360
+ })
361
+ .then(function (res) { return res.json(); })
362
+ .then(function (data) { return { ok: true, response: data }; })
363
+ .catch(function (err) { return { ok: false, error: err.message }; });
364
+ }
365
+
366
+ function httpTransportReceive(opts) {
367
+ var hubUrl = (opts && opts.hubUrl) || process.env.A2A_HUB_URL;
368
+ if (!hubUrl) return Promise.resolve([]);
369
+ var assetType = (opts && opts.assetType) || null;
370
+ var signals = (opts && Array.isArray(opts.signals)) ? opts.signals : null;
371
+ var fetchMsg = buildFetch({ assetType: assetType, signals: signals });
372
+ var endpoint = hubUrl.replace(/\/+$/, '') + '/a2a/fetch';
373
+ return fetch(endpoint, {
374
+ method: 'POST',
375
+ headers: buildHubHeaders(),
376
+ body: JSON.stringify(fetchMsg),
377
+ })
378
+ .then(function (res) { return res.json(); })
379
+ .then(function (data) {
380
+ if (data && data.payload && Array.isArray(data.payload.results)) {
381
+ return data.payload.results;
382
+ }
383
+ return [];
384
+ })
385
+ .catch(function () { return []; });
386
+ }
387
+
388
+ function httpTransportList() {
389
+ return ['http'];
390
+ }
391
+
392
+ // --- Heartbeat ---
393
+
394
+ var _heartbeatTimer = null;
395
+ var _heartbeatStartedAt = null;
396
+ var _heartbeatConsecutiveFailures = 0;
397
+ var _heartbeatTotalSent = 0;
398
+ var _heartbeatTotalFailed = 0;
399
+ var _latestAvailableWork = [];
400
+ var _latestOverdueTasks = [];
401
+ var _pendingCommitmentUpdates = [];
402
+ var _cachedHubNodeSecret = null;
403
+ var _heartbeatIntervalMs = 0;
404
+ var _heartbeatRunning = false;
405
+
406
+ var NODE_SECRET_FILE = path.join(NODE_ID_DIR, 'node_secret');
407
+
408
+ function _loadPersistedNodeSecret() {
409
+ try {
410
+ if (fs.existsSync(NODE_SECRET_FILE)) {
411
+ var s = fs.readFileSync(NODE_SECRET_FILE, 'utf8').trim();
412
+ if (s && /^[a-f0-9]{64}$/i.test(s)) return s;
413
+ }
414
+ } catch {}
415
+ return null;
416
+ }
417
+
418
+ function _persistNodeSecret(secret) {
419
+ try {
420
+ if (!fs.existsSync(NODE_ID_DIR)) {
421
+ fs.mkdirSync(NODE_ID_DIR, { recursive: true, mode: 0o700 });
422
+ }
423
+ fs.writeFileSync(NODE_SECRET_FILE, secret, { encoding: 'utf8', mode: 0o600 });
424
+ } catch {}
425
+ }
426
+
427
+ function getHubUrl() {
428
+ return process.env.A2A_HUB_URL || process.env.EVOMAP_HUB_URL || '';
429
+ }
430
+
431
+ function buildHubHeaders() {
432
+ var headers = { 'Content-Type': 'application/json' };
433
+ var secret = getHubNodeSecret();
434
+ if (secret) headers['Authorization'] = 'Bearer ' + secret;
435
+ return headers;
436
+ }
437
+
438
+ function sendHelloToHub() {
439
+ var hubUrl = getHubUrl();
440
+ if (!hubUrl) return Promise.resolve({ ok: false, error: 'no_hub_url' });
441
+
442
+ var endpoint = hubUrl.replace(/\/+$/, '') + '/a2a/hello';
443
+ var nodeId = getNodeId();
444
+ var msg = buildHello({ nodeId: nodeId, capabilities: {} });
445
+ msg.sender_id = nodeId;
446
+
447
+ return fetch(endpoint, {
448
+ method: 'POST',
449
+ headers: buildHubHeaders(),
450
+ body: JSON.stringify(msg),
451
+ signal: AbortSignal.timeout(15000),
452
+ })
453
+ .then(function (res) { return res.json(); })
454
+ .then(function (data) {
455
+ var secret = (data && data.payload && data.payload.node_secret)
456
+ || (data && data.node_secret)
457
+ || null;
458
+ if (secret && /^[a-f0-9]{64}$/i.test(secret)) {
459
+ _cachedHubNodeSecret = secret;
460
+ _persistNodeSecret(secret);
461
+ }
462
+ return { ok: true, response: data };
463
+ })
464
+ .catch(function (err) { return { ok: false, error: err.message }; });
465
+ }
466
+
467
+ function getHubNodeSecret() {
468
+ if (_cachedHubNodeSecret) return _cachedHubNodeSecret;
469
+ var persisted = _loadPersistedNodeSecret();
470
+ if (persisted) {
471
+ _cachedHubNodeSecret = persisted;
472
+ return persisted;
473
+ }
474
+ return null;
475
+ }
476
+
477
+ function _scheduleNextHeartbeat(delayMs) {
478
+ if (!_heartbeatRunning) return;
479
+ if (_heartbeatTimer) clearTimeout(_heartbeatTimer);
480
+ var delay = delayMs || _heartbeatIntervalMs;
481
+ _heartbeatTimer = setTimeout(function () {
482
+ if (!_heartbeatRunning) return;
483
+ sendHeartbeat().catch(function () {});
484
+ _scheduleNextHeartbeat();
485
+ }, delay);
486
+ if (_heartbeatTimer.unref) _heartbeatTimer.unref();
487
+ }
488
+
489
+ function sendHeartbeat() {
490
+ var hubUrl = getHubUrl();
491
+ if (!hubUrl) return Promise.resolve({ ok: false, error: 'no_hub_url' });
492
+
493
+ var endpoint = hubUrl.replace(/\/+$/, '') + '/a2a/heartbeat';
494
+ var nodeId = getNodeId();
495
+ var bodyObj = {
496
+ node_id: nodeId,
497
+ sender_id: nodeId,
498
+ version: PROTOCOL_VERSION,
499
+ uptime_ms: _heartbeatStartedAt ? Date.now() - _heartbeatStartedAt : 0,
500
+ timestamp: new Date().toISOString(),
501
+ };
502
+
503
+ var meta = {};
504
+
505
+ if (process.env.WORKER_ENABLED === '1') {
506
+ var domains = (process.env.WORKER_DOMAINS || '').split(',').map(function (s) { return s.trim(); }).filter(Boolean);
507
+ meta.worker_enabled = true;
508
+ meta.worker_domains = domains;
509
+ meta.max_load = Math.max(1, Number(process.env.WORKER_MAX_LOAD) || 5);
510
+ }
511
+
512
+ if (_pendingCommitmentUpdates.length > 0) {
513
+ meta.commitment_updates = _pendingCommitmentUpdates.splice(0);
514
+ }
515
+
516
+ if (Object.keys(meta).length > 0) {
517
+ bodyObj.meta = meta;
518
+ }
519
+
520
+ var body = JSON.stringify(bodyObj);
521
+
522
+ _heartbeatTotalSent++;
523
+
524
+ return fetch(endpoint, {
525
+ method: 'POST',
526
+ headers: buildHubHeaders(),
527
+ body: body,
528
+ signal: AbortSignal.timeout(10000),
529
+ })
530
+ .then(function (res) { return res.json(); })
531
+ .then(function (data) {
532
+ if (data && (data.error === 'rate_limited' || data.status === 'rate_limited')) {
533
+ var retryMs = Number(data.retry_after_ms) || 0;
534
+ var policy = data.policy || {};
535
+ var windowMs = Number(policy.window_ms) || 0;
536
+ var backoff = retryMs > 0 ? retryMs + 5000 : (windowMs > 0 ? windowMs + 5000 : _heartbeatIntervalMs);
537
+ if (backoff > _heartbeatIntervalMs) {
538
+ console.warn('[Heartbeat] Rate limited by hub. Next attempt in ' + Math.round(backoff / 1000) + 's. ' +
539
+ 'Consider increasing HEARTBEAT_INTERVAL_MS to >= ' + (windowMs || backoff) + 'ms.');
540
+ _scheduleNextHeartbeat(backoff);
541
+ }
542
+ return { ok: false, error: 'rate_limited', retryMs: backoff };
543
+ }
544
+ if (data && data.status === 'unknown_node') {
545
+ console.warn('[Heartbeat] Node not registered on hub. Sending hello to re-register...');
546
+ return sendHelloToHub().then(function (helloResult) {
547
+ if (helloResult.ok) {
548
+ console.log('[Heartbeat] Re-registered with hub successfully.');
549
+ _heartbeatConsecutiveFailures = 0;
550
+ } else {
551
+ console.warn('[Heartbeat] Re-registration failed: ' + (helloResult.error || 'unknown'));
552
+ }
553
+ return { ok: helloResult.ok, response: data, reregistered: helloResult.ok };
554
+ });
555
+ }
556
+ if (Array.isArray(data.available_work)) {
557
+ _latestAvailableWork = data.available_work;
558
+ }
559
+ if (Array.isArray(data.overdue_tasks) && data.overdue_tasks.length > 0) {
560
+ _latestOverdueTasks = data.overdue_tasks;
561
+ console.warn('[Commitment] ' + data.overdue_tasks.length + ' overdue task(s) detected via heartbeat.');
562
+ }
563
+ _heartbeatConsecutiveFailures = 0;
564
+ try {
565
+ var logPath = getEvolverLogPath();
566
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
567
+ var now = new Date();
568
+ try {
569
+ fs.utimesSync(logPath, now, now);
570
+ } catch (e) {
571
+ if (e && e.code === 'ENOENT') {
572
+ try {
573
+ var fd = fs.openSync(logPath, 'a');
574
+ fs.closeSync(fd);
575
+ fs.utimesSync(logPath, now, now);
576
+ } catch (innerErr) {
577
+ console.warn('[Heartbeat] Failed to create evolver_loop.log: ' + innerErr.message);
578
+ }
579
+ } else {
580
+ console.warn('[Heartbeat] Failed to touch evolver_loop.log: ' + e.message);
581
+ }
582
+ }
583
+ } catch (outerErr) {
584
+ console.warn('[Heartbeat] Failed to ensure evolver_loop.log: ' + outerErr.message);
585
+ }
586
+ return { ok: true, response: data };
587
+ })
588
+ .catch(function (err) {
589
+ _heartbeatConsecutiveFailures++;
590
+ _heartbeatTotalFailed++;
591
+ if (_heartbeatConsecutiveFailures === 3) {
592
+ console.warn('[Heartbeat] 3 consecutive failures. Network issue? Last error: ' + err.message);
593
+ } else if (_heartbeatConsecutiveFailures === 10) {
594
+ console.warn('[Heartbeat] 10 consecutive failures. Hub may be unreachable. (' + err.message + ')');
595
+ } else if (_heartbeatConsecutiveFailures % 50 === 0) {
596
+ console.warn('[Heartbeat] ' + _heartbeatConsecutiveFailures + ' consecutive failures. (' + err.message + ')');
597
+ }
598
+ return { ok: false, error: err.message };
599
+ });
600
+ }
601
+
602
+ function getLatestAvailableWork() {
603
+ return _latestAvailableWork;
604
+ }
605
+
606
+ function consumeAvailableWork() {
607
+ var work = _latestAvailableWork;
608
+ _latestAvailableWork = [];
609
+ return work;
610
+ }
611
+
612
+ function getOverdueTasks() {
613
+ return _latestOverdueTasks;
614
+ }
615
+
616
+ function consumeOverdueTasks() {
617
+ var tasks = _latestOverdueTasks;
618
+ _latestOverdueTasks = [];
619
+ return tasks;
620
+ }
621
+
622
+ /**
623
+ * Queue a commitment deadline update to be sent with the next heartbeat.
624
+ * @param {string} taskId
625
+ * @param {string} deadlineIso - ISO-8601 deadline
626
+ * @param {boolean} [isAssignment] - true if this is a WorkAssignment
627
+ */
628
+ function queueCommitmentUpdate(taskId, deadlineIso, isAssignment) {
629
+ if (!taskId || !deadlineIso) return;
630
+ _pendingCommitmentUpdates.push({
631
+ task_id: taskId,
632
+ deadline: deadlineIso,
633
+ assignment: !!isAssignment,
634
+ });
635
+ }
636
+
637
+ function startHeartbeat(intervalMs) {
638
+ if (_heartbeatRunning) return;
639
+ _heartbeatIntervalMs = intervalMs || Number(process.env.HEARTBEAT_INTERVAL_MS) || 360000; // default 6min
640
+ _heartbeatStartedAt = Date.now();
641
+ _heartbeatRunning = true;
642
+
643
+ sendHelloToHub().then(function (r) {
644
+ if (r.ok) console.log('[Heartbeat] Registered with hub. Node: ' + getNodeId());
645
+ else console.warn('[Heartbeat] Hello failed (will retry via heartbeat): ' + (r.error || 'unknown'));
646
+ }).catch(function () {}).then(function () {
647
+ if (!_heartbeatRunning) return;
648
+ // First heartbeat after hello completes, with enough gap to avoid rate limit
649
+ _scheduleNextHeartbeat(Math.max(30000, _heartbeatIntervalMs));
650
+ });
651
+ }
652
+
653
+ function stopHeartbeat() {
654
+ _heartbeatRunning = false;
655
+ if (_heartbeatTimer) {
656
+ clearTimeout(_heartbeatTimer);
657
+ _heartbeatTimer = null;
658
+ }
659
+ }
660
+
661
+ function getHeartbeatStats() {
662
+ return {
663
+ running: _heartbeatRunning,
664
+ uptimeMs: _heartbeatStartedAt ? Date.now() - _heartbeatStartedAt : 0,
665
+ totalSent: _heartbeatTotalSent,
666
+ totalFailed: _heartbeatTotalFailed,
667
+ consecutiveFailures: _heartbeatConsecutiveFailures,
668
+ };
669
+ }
670
+
671
+ // --- Transport registry ---
672
+
673
+ var transports = {
674
+ file: {
675
+ send: fileTransportSend,
676
+ receive: fileTransportReceive,
677
+ list: fileTransportList,
678
+ },
679
+ http: {
680
+ send: httpTransportSend,
681
+ receive: httpTransportReceive,
682
+ list: httpTransportList,
683
+ },
684
+ };
685
+
686
+ function getTransport(name) {
687
+ var n = String(name || process.env.A2A_TRANSPORT || 'file').toLowerCase();
688
+ var t = transports[n];
689
+ if (!t) throw new Error('Unknown A2A transport: ' + n + '. Available: ' + Object.keys(transports).join(', '));
690
+ return t;
691
+ }
692
+
693
+ function registerTransport(name, impl) {
694
+ if (!name || typeof name !== 'string') throw new Error('transport name required');
695
+ if (!impl || typeof impl.send !== 'function' || typeof impl.receive !== 'function') {
696
+ throw new Error('transport must implement send() and receive()');
697
+ }
698
+ transports[name] = impl;
699
+ }
700
+
701
+ module.exports = {
702
+ PROTOCOL_NAME,
703
+ PROTOCOL_VERSION,
704
+ VALID_MESSAGE_TYPES,
705
+ getNodeId,
706
+ buildMessage,
707
+ buildHello,
708
+ buildPublish,
709
+ buildPublishBundle,
710
+ buildFetch,
711
+ buildReport,
712
+ buildDecision,
713
+ buildRevoke,
714
+ isValidProtocolMessage,
715
+ unwrapAssetFromMessage,
716
+ getTransport,
717
+ registerTransport,
718
+ fileTransportSend,
719
+ fileTransportReceive,
720
+ fileTransportList,
721
+ httpTransportSend,
722
+ httpTransportReceive,
723
+ httpTransportList,
724
+ sendHeartbeat,
725
+ sendHelloToHub,
726
+ startHeartbeat,
727
+ stopHeartbeat,
728
+ getHeartbeatStats,
729
+ getLatestAvailableWork,
730
+ consumeAvailableWork,
731
+ getOverdueTasks,
732
+ consumeOverdueTasks,
733
+ queueCommitmentUpdate,
734
+ getHubNodeSecret,
735
+ buildHubHeaders,
736
+ };