@noosphere/agent-core 0.1.0-alpha.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/index.cjs ADDED
@@ -0,0 +1,1915 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ CommitmentUtils: () => CommitmentUtils,
34
+ ConfigLoader: () => ConfigLoader,
35
+ ContainerManager: () => ContainerManager,
36
+ EventMonitor: () => EventMonitor,
37
+ FulfillResult: () => FulfillResult,
38
+ KeystoreManager: () => import_crypto2.KeystoreManager,
39
+ NoosphereAgent: () => NoosphereAgent,
40
+ RegistryManager: () => import_registry2.RegistryManager,
41
+ RequestIdUtils: () => RequestIdUtils,
42
+ SchedulerService: () => SchedulerService,
43
+ WalletManager: () => import_crypto2.WalletManager
44
+ });
45
+ module.exports = __toCommonJS(index_exports);
46
+
47
+ // src/EventMonitor.ts
48
+ var import_events = require("events");
49
+ var import_ethers = require("ethers");
50
+ var EventMonitor = class extends import_events.EventEmitter {
51
+ constructor(config, routerAbi, coordinatorAbi, options) {
52
+ super();
53
+ this.config = config;
54
+ this.routerAbi = routerAbi;
55
+ this.coordinatorAbi = coordinatorAbi;
56
+ this.reconnectAttempts = 0;
57
+ this.maxReconnectAttempts = 10;
58
+ this.isReconnecting = false;
59
+ this.heartbeatInterval = null;
60
+ this.lastEventTime = Date.now();
61
+ this.lastProcessedBlock = config.deploymentBlock || 0;
62
+ this.useWebSocket = false;
63
+ this.checkpointCallbacks = options;
64
+ }
65
+ async connect() {
66
+ try {
67
+ if (this.config.wsRpcUrl) {
68
+ this.provider = new import_ethers.ethers.WebSocketProvider(this.config.wsRpcUrl);
69
+ this.useWebSocket = true;
70
+ console.log("\u2713 Connected via WebSocket (push-based events)");
71
+ } else {
72
+ throw new Error("WebSocket URL not provided");
73
+ }
74
+ } catch (error) {
75
+ console.warn("WebSocket unavailable, falling back to HTTP polling");
76
+ this.provider = new import_ethers.ethers.JsonRpcProvider(this.config.rpcUrl);
77
+ this.useWebSocket = false;
78
+ }
79
+ this.router = new import_ethers.ethers.Contract(this.config.routerAddress, this.routerAbi, this.provider);
80
+ this.coordinator = new import_ethers.ethers.Contract(
81
+ this.config.coordinatorAddress,
82
+ this.coordinatorAbi,
83
+ this.provider
84
+ );
85
+ }
86
+ async start() {
87
+ if (this.checkpointCallbacks?.loadCheckpoint) {
88
+ const checkpoint = this.checkpointCallbacks.loadCheckpoint();
89
+ if (checkpoint) {
90
+ this.lastProcessedBlock = checkpoint.blockNumber;
91
+ }
92
+ }
93
+ console.log(`Starting from block ${this.lastProcessedBlock}`);
94
+ await this.replayEvents(this.lastProcessedBlock, "latest");
95
+ if (this.useWebSocket) {
96
+ await this.startWebSocketListening();
97
+ } else {
98
+ await this.startPolling();
99
+ }
100
+ }
101
+ async replayEvents(fromBlock, toBlock) {
102
+ console.log(`Replaying events from block ${fromBlock} to ${toBlock}`);
103
+ const currentBlock = await this.provider.getBlockNumber();
104
+ const toBlockNumber = toBlock === "latest" ? currentBlock : Number(toBlock);
105
+ const chunkSize = 1e4;
106
+ for (let start = fromBlock; start <= toBlockNumber; start += chunkSize) {
107
+ const end = Math.min(start + chunkSize - 1, toBlockNumber);
108
+ const events = await this.coordinator.queryFilter(
109
+ this.coordinator.filters.RequestStarted(),
110
+ start,
111
+ end
112
+ );
113
+ for (const event of events) {
114
+ await this.processEvent(event);
115
+ }
116
+ if (events.length > 0) {
117
+ this.saveCheckpoint(end);
118
+ }
119
+ }
120
+ console.log(`Replayed events up to block ${toBlockNumber}`);
121
+ }
122
+ async startWebSocketListening() {
123
+ console.log("Starting WebSocket event listening...");
124
+ this.coordinator.on("RequestStarted", async (...args) => {
125
+ const event = args[args.length - 1];
126
+ this.lastEventTime = Date.now();
127
+ await this.processEvent(event);
128
+ const blockNumber = event.blockNumber;
129
+ if (blockNumber - this.lastProcessedBlock >= 10) {
130
+ this.saveCheckpoint(blockNumber);
131
+ }
132
+ });
133
+ if (this.provider instanceof import_ethers.ethers.WebSocketProvider) {
134
+ const wsProvider = this.provider;
135
+ const ws = wsProvider._websocket;
136
+ if (ws) {
137
+ ws.on("close", () => {
138
+ console.warn("\u26A0\uFE0F WebSocket connection closed");
139
+ this.handleDisconnect();
140
+ });
141
+ ws.on("error", (error) => {
142
+ console.error("\u26A0\uFE0F WebSocket error:", error.message);
143
+ this.handleDisconnect();
144
+ });
145
+ }
146
+ }
147
+ this.startHeartbeat();
148
+ console.log("\u2713 WebSocket event listener started");
149
+ }
150
+ startHeartbeat() {
151
+ this.heartbeatInterval = setInterval(async () => {
152
+ try {
153
+ if (this.provider instanceof import_ethers.ethers.WebSocketProvider) {
154
+ const blockNumber = await this.provider.getBlockNumber();
155
+ const timeSinceLastEvent = Date.now() - this.lastEventTime;
156
+ if (timeSinceLastEvent > 18e4 && blockNumber > this.lastProcessedBlock + 5) {
157
+ console.log(`\u26A0\uFE0F No events for ${Math.round(timeSinceLastEvent / 1e3)}s, checking for missed events...`);
158
+ await this.replayMissedEvents();
159
+ }
160
+ }
161
+ } catch (error) {
162
+ console.error("\u26A0\uFE0F Heartbeat failed, WebSocket may be disconnected");
163
+ this.handleDisconnect();
164
+ }
165
+ }, 12e4);
166
+ }
167
+ async replayMissedEvents() {
168
+ try {
169
+ const currentBlock = await this.provider.getBlockNumber();
170
+ if (currentBlock > this.lastProcessedBlock) {
171
+ console.log(`\u{1F4E5} Replaying events from block ${this.lastProcessedBlock + 1} to ${currentBlock}`);
172
+ await this.replayEvents(this.lastProcessedBlock + 1, currentBlock);
173
+ this.lastEventTime = Date.now();
174
+ }
175
+ } catch (error) {
176
+ console.error("Failed to replay missed events:", error);
177
+ }
178
+ }
179
+ handleDisconnect() {
180
+ if (this.isReconnecting) return;
181
+ this.isReconnecting = true;
182
+ if (this.heartbeatInterval) {
183
+ clearInterval(this.heartbeatInterval);
184
+ this.heartbeatInterval = null;
185
+ }
186
+ this.reconnect().finally(() => {
187
+ this.isReconnecting = false;
188
+ });
189
+ }
190
+ async startPolling() {
191
+ console.log("Starting HTTP polling (fallback mode)...");
192
+ const pollingInterval = this.config.pollingInterval || 12e3;
193
+ let lastBlock = await this.provider.getBlockNumber();
194
+ setInterval(async () => {
195
+ try {
196
+ const currentBlock = await this.provider.getBlockNumber();
197
+ if (currentBlock > lastBlock) {
198
+ const events = await this.coordinator.queryFilter(
199
+ this.coordinator.filters.RequestStarted(),
200
+ lastBlock + 1,
201
+ currentBlock
202
+ );
203
+ for (const event of events) {
204
+ await this.processEvent(event);
205
+ }
206
+ if (events.length > 0) {
207
+ this.saveCheckpoint(currentBlock);
208
+ }
209
+ lastBlock = currentBlock;
210
+ }
211
+ } catch (error) {
212
+ console.error("Polling error:", error);
213
+ }
214
+ }, pollingInterval);
215
+ }
216
+ async processEvent(event) {
217
+ const commitment = event.args.commitment;
218
+ const requestStartedEvent = {
219
+ requestId: event.args.requestId,
220
+ subscriptionId: event.args.subscriptionId,
221
+ containerId: event.args.containerId,
222
+ interval: commitment.interval,
223
+ redundancy: commitment.redundancy,
224
+ useDeliveryInbox: commitment.useDeliveryInbox,
225
+ feeAmount: commitment.feeAmount,
226
+ feeToken: commitment.feeToken,
227
+ verifier: commitment.verifier,
228
+ coordinator: commitment.coordinator,
229
+ walletAddress: commitment.walletAddress,
230
+ blockNumber: event.blockNumber
231
+ };
232
+ this.emit("RequestStarted", requestStartedEvent);
233
+ }
234
+ async reconnect() {
235
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
236
+ console.error("Max reconnection attempts reached. Falling back to HTTP polling.");
237
+ this.useWebSocket = false;
238
+ await this.connect();
239
+ await this.startPolling();
240
+ return;
241
+ }
242
+ const backoff = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 6e4);
243
+ console.log(
244
+ `\u{1F504} Reconnecting in ${backoff}ms (attempt ${this.reconnectAttempts + 1}/${this.maxReconnectAttempts})`
245
+ );
246
+ await new Promise((resolve) => setTimeout(resolve, backoff));
247
+ try {
248
+ if (this.coordinator) {
249
+ this.coordinator.removeAllListeners();
250
+ }
251
+ if (this.provider instanceof import_ethers.ethers.WebSocketProvider) {
252
+ try {
253
+ await this.provider.destroy();
254
+ } catch {
255
+ }
256
+ }
257
+ await this.connect();
258
+ await this.replayMissedEvents();
259
+ await this.startWebSocketListening();
260
+ console.log("\u2713 Reconnected successfully");
261
+ this.reconnectAttempts = 0;
262
+ this.lastEventTime = Date.now();
263
+ } catch (error) {
264
+ console.error("Reconnection failed:", error);
265
+ this.reconnectAttempts++;
266
+ await this.reconnect();
267
+ }
268
+ }
269
+ saveCheckpoint(blockNumber) {
270
+ this.lastProcessedBlock = blockNumber;
271
+ if (this.checkpointCallbacks?.saveCheckpoint) {
272
+ this.checkpointCallbacks.saveCheckpoint({
273
+ blockNumber,
274
+ blockTimestamp: Date.now()
275
+ });
276
+ }
277
+ }
278
+ async stop() {
279
+ if (this.heartbeatInterval) {
280
+ clearInterval(this.heartbeatInterval);
281
+ this.heartbeatInterval = null;
282
+ }
283
+ if (this.router) {
284
+ this.router.removeAllListeners();
285
+ }
286
+ if (this.coordinator) {
287
+ this.coordinator.removeAllListeners();
288
+ }
289
+ if (this.provider instanceof import_ethers.ethers.WebSocketProvider) {
290
+ await this.provider.destroy();
291
+ }
292
+ }
293
+ };
294
+
295
+ // src/ContainerManager.ts
296
+ var import_dockerode = __toESM(require("dockerode"));
297
+ var import_promises = __toESM(require("fs/promises"));
298
+ var import_path = __toESM(require("path"));
299
+ var import_axios = __toESM(require("axios"));
300
+ var ContainerManager = class {
301
+ constructor() {
302
+ this.runningContainers = /* @__PURE__ */ new Set();
303
+ this.persistentContainers = /* @__PURE__ */ new Map();
304
+ this.containerPorts = /* @__PURE__ */ new Map();
305
+ this.docker = new import_dockerode.default();
306
+ }
307
+ async runContainer(container, input, timeout = 3e5) {
308
+ const startTime = Date.now();
309
+ try {
310
+ const port = container.port ? parseInt(container.port) : 8081;
311
+ const url = `http://localhost:${port}/computation`;
312
+ let requestBody;
313
+ try {
314
+ const parsedInput = JSON.parse(input);
315
+ requestBody = { input, ...parsedInput };
316
+ } catch {
317
+ requestBody = { input };
318
+ }
319
+ const response = await import_axios.default.post(url, requestBody, {
320
+ timeout,
321
+ headers: {
322
+ "Content-Type": "application/json"
323
+ }
324
+ });
325
+ const executionTime = Date.now() - startTime;
326
+ let output;
327
+ if (typeof response.data === "string") {
328
+ output = response.data;
329
+ } else if (response.data.output !== void 0) {
330
+ output = typeof response.data.output === "string" ? response.data.output : JSON.stringify(response.data.output);
331
+ } else {
332
+ output = JSON.stringify(response.data);
333
+ }
334
+ return {
335
+ output,
336
+ exitCode: 0,
337
+ executionTime
338
+ };
339
+ } catch (error) {
340
+ const executionTime = Date.now() - startTime;
341
+ if (error.response) {
342
+ throw new Error(
343
+ `Container HTTP error ${error.response.status}: ${JSON.stringify(error.response.data)}`
344
+ );
345
+ } else if (error.code === "ECONNREFUSED") {
346
+ throw new Error(
347
+ `Cannot connect to container (port ${container.port || 8081}). Is it running?`
348
+ );
349
+ } else if (error.code === "ETIMEDOUT" || error.code === "ECONNABORTED") {
350
+ throw new Error(`Container execution timeout after ${timeout}ms`);
351
+ }
352
+ throw error;
353
+ }
354
+ }
355
+ async collectContainerResult(dockerContainer, workDir, startTime) {
356
+ try {
357
+ const inspectData = await dockerContainer.inspect();
358
+ const exitCode = inspectData.State.ExitCode || 0;
359
+ const outputPath = import_path.default.join(workDir, "output.json");
360
+ let output = "";
361
+ try {
362
+ output = await import_promises.default.readFile(outputPath, "utf-8");
363
+ } catch (error) {
364
+ const logs = await dockerContainer.logs({
365
+ stdout: true,
366
+ stderr: true
367
+ });
368
+ output = logs.toString();
369
+ }
370
+ const executionTime = Date.now() - startTime;
371
+ try {
372
+ await dockerContainer.remove({ force: true });
373
+ } catch (error) {
374
+ }
375
+ await import_promises.default.rm(workDir, { recursive: true, force: true });
376
+ return {
377
+ output,
378
+ exitCode,
379
+ executionTime
380
+ };
381
+ } catch (error) {
382
+ try {
383
+ await dockerContainer.remove({ force: true });
384
+ } catch {
385
+ }
386
+ await import_promises.default.rm(workDir, { recursive: true, force: true }).catch(() => {
387
+ });
388
+ throw error;
389
+ }
390
+ }
391
+ async pullImage(image, tag) {
392
+ const imageTag = `${image}:${tag}`;
393
+ try {
394
+ await this.docker.getImage(imageTag).inspect();
395
+ console.log(`Image ${imageTag} already exists`);
396
+ } catch {
397
+ console.log(`Pulling image ${imageTag}...`);
398
+ return new Promise((resolve, reject) => {
399
+ this.docker.pull(imageTag, (err, stream) => {
400
+ if (err) return reject(err);
401
+ this.docker.modem.followProgress(
402
+ stream,
403
+ (err2) => {
404
+ if (err2) return reject(err2);
405
+ console.log(`\u2713 Pulled image ${imageTag}`);
406
+ resolve();
407
+ },
408
+ (event) => {
409
+ if (event.status) {
410
+ console.log(`${event.status} ${event.progress || ""}`);
411
+ }
412
+ }
413
+ );
414
+ });
415
+ });
416
+ }
417
+ }
418
+ async waitForContainer(container) {
419
+ return new Promise((resolve, reject) => {
420
+ container.wait((err, data) => {
421
+ if (err) return reject(err);
422
+ resolve(data);
423
+ });
424
+ });
425
+ }
426
+ timeout(ms) {
427
+ return new Promise((resolve) => setTimeout(() => resolve(null), ms));
428
+ }
429
+ parseMemory(memory) {
430
+ const units = {
431
+ b: 1,
432
+ kb: 1024,
433
+ mb: 1024 * 1024,
434
+ gb: 1024 * 1024 * 1024
435
+ };
436
+ const match = memory.toLowerCase().match(/^(\d+)\s*(b|kb|mb|gb)$/);
437
+ if (!match) {
438
+ throw new Error(`Invalid memory format: ${memory}`);
439
+ }
440
+ const [, value, unit] = match;
441
+ return parseInt(value, 10) * units[unit];
442
+ }
443
+ async checkDockerAvailable() {
444
+ try {
445
+ await this.docker.ping();
446
+ return true;
447
+ } catch {
448
+ return false;
449
+ }
450
+ }
451
+ async getDockerInfo() {
452
+ return this.docker.info();
453
+ }
454
+ /**
455
+ * Cleanup all running containers
456
+ * Called when agent is shutting down
457
+ */
458
+ async cleanup() {
459
+ if (this.runningContainers.size === 0) {
460
+ return;
461
+ }
462
+ console.log(`\u{1F9F9} Cleaning up ${this.runningContainers.size} running containers...`);
463
+ const cleanupPromises = Array.from(this.runningContainers).map(async (container) => {
464
+ try {
465
+ const inspect = await container.inspect();
466
+ if (inspect.State.Running) {
467
+ console.log(` Stopping container ${inspect.Id.slice(0, 12)}...`);
468
+ await container.stop({ t: 10 });
469
+ await container.remove({ force: true });
470
+ }
471
+ } catch (error) {
472
+ console.warn(` Warning: Failed to cleanup container:`, error.message);
473
+ }
474
+ });
475
+ await Promise.all(cleanupPromises);
476
+ this.runningContainers.clear();
477
+ console.log("\u2713 Container cleanup completed");
478
+ }
479
+ /**
480
+ * Get number of running containers
481
+ */
482
+ getRunningContainerCount() {
483
+ return this.runningContainers.size;
484
+ }
485
+ /**
486
+ * Pre-pull container images and start persistent containers on startup
487
+ * This speeds up request handling by having containers ready
488
+ */
489
+ async prepareContainers(containers) {
490
+ if (containers.size === 0) {
491
+ console.log("No containers to prepare");
492
+ return;
493
+ }
494
+ console.log(`
495
+ \u{1F680} Preparing ${containers.size} containers...`);
496
+ const pullAndStartPromises = Array.from(containers.entries()).map(async ([id, container]) => {
497
+ try {
498
+ const imageTag = `${container.image}:${container.tag || "latest"}`;
499
+ console.log(` Pulling ${imageTag}...`);
500
+ await this.pullImage(container.image, container.tag || "latest");
501
+ console.log(` \u2713 ${imageTag} ready`);
502
+ await this.startPersistentContainer(id, container);
503
+ } catch (error) {
504
+ console.error(` \u274C Failed to prepare ${container.image}:`, error.message);
505
+ }
506
+ });
507
+ await Promise.all(pullAndStartPromises);
508
+ console.log("\u2713 All containers prepared\n");
509
+ }
510
+ /**
511
+ * Start a persistent container that stays running
512
+ */
513
+ async startPersistentContainer(containerId, metadata) {
514
+ const containerName = `noosphere-${containerId}`;
515
+ const imageTag = `${metadata.image}:${metadata.tag || "latest"}`;
516
+ try {
517
+ const existingContainer = this.docker.getContainer(containerName);
518
+ try {
519
+ const inspect = await existingContainer.inspect();
520
+ if (inspect.State.Running) {
521
+ console.log(` \u2713 Container ${containerName} already running`);
522
+ this.persistentContainers.set(containerId, existingContainer);
523
+ return;
524
+ } else {
525
+ await existingContainer.start();
526
+ console.log(` \u2713 Started existing container ${containerName}`);
527
+ this.persistentContainers.set(containerId, existingContainer);
528
+ return;
529
+ }
530
+ } catch (err) {
531
+ }
532
+ } catch (err) {
533
+ }
534
+ const createOptions = {
535
+ name: containerName,
536
+ Image: imageTag,
537
+ Tty: false,
538
+ AttachStdout: false,
539
+ AttachStderr: false,
540
+ ExposedPorts: metadata.port ? { [`${metadata.port}/tcp`]: {} } : void 0,
541
+ HostConfig: {
542
+ AutoRemove: false,
543
+ // Keep container for reuse
544
+ PortBindings: metadata.port ? {
545
+ [`${metadata.port}/tcp`]: [{ HostPort: metadata.port }]
546
+ } : void 0
547
+ },
548
+ Env: metadata.env ? Object.entries(metadata.env).map(([k, v]) => `${k}=${v}`) : void 0
549
+ };
550
+ if (metadata.requirements) {
551
+ const resources = {};
552
+ if (metadata.requirements.memory) {
553
+ resources.Memory = this.parseMemory(metadata.requirements.memory);
554
+ }
555
+ if (metadata.requirements.cpu) {
556
+ resources.NanoCpus = metadata.requirements.cpu * 1e9;
557
+ }
558
+ if (metadata.requirements.gpu) {
559
+ createOptions.HostConfig.DeviceRequests = [
560
+ {
561
+ Driver: "nvidia",
562
+ Count: -1,
563
+ Capabilities: [["gpu"]]
564
+ }
565
+ ];
566
+ }
567
+ if (Object.keys(resources).length > 0) {
568
+ createOptions.HostConfig = {
569
+ ...createOptions.HostConfig,
570
+ ...resources
571
+ };
572
+ }
573
+ }
574
+ const dockerContainer = await this.docker.createContainer(createOptions);
575
+ await dockerContainer.start();
576
+ this.persistentContainers.set(containerId, dockerContainer);
577
+ if (metadata.port) {
578
+ this.containerPorts.set(containerId, parseInt(metadata.port));
579
+ }
580
+ console.log(` \u2713 Started persistent container ${containerName}`);
581
+ }
582
+ /**
583
+ * Stop and remove all persistent containers
584
+ */
585
+ async stopPersistentContainers() {
586
+ if (this.persistentContainers.size === 0) {
587
+ return;
588
+ }
589
+ console.log(`
590
+ \u{1F6D1} Stopping ${this.persistentContainers.size} persistent containers...`);
591
+ const stopPromises = Array.from(this.persistentContainers.entries()).map(
592
+ async ([id, container]) => {
593
+ try {
594
+ const inspect = await container.inspect();
595
+ if (inspect.State.Running) {
596
+ console.log(` Stopping ${inspect.Name}...`);
597
+ await container.stop({ t: 10 });
598
+ }
599
+ await container.remove({ force: true });
600
+ console.log(` \u2713 Stopped ${inspect.Name}`);
601
+ } catch (error) {
602
+ console.warn(` Warning: Failed to stop container ${id}:`, error.message);
603
+ }
604
+ }
605
+ );
606
+ await Promise.all(stopPromises);
607
+ this.persistentContainers.clear();
608
+ this.containerPorts.clear();
609
+ console.log("\u2713 All persistent containers stopped\n");
610
+ }
611
+ };
612
+
613
+ // src/NoosphereAgent.ts
614
+ var import_ethers4 = require("ethers");
615
+
616
+ // src/SchedulerService.ts
617
+ var import_events2 = require("events");
618
+ var import_contracts = require("@noosphere/contracts");
619
+ var SchedulerService = class extends import_events2.EventEmitter {
620
+ constructor(provider, router, coordinator, agentWallet, batchReaderAddress, config, getContainer) {
621
+ super();
622
+ this.provider = provider;
623
+ this.router = router;
624
+ this.coordinator = coordinator;
625
+ this.agentWallet = agentWallet;
626
+ this.subscriptions = /* @__PURE__ */ new Map();
627
+ this.committedIntervals = /* @__PURE__ */ new Set();
628
+ // subscriptionId:interval
629
+ this.pendingTxs = /* @__PURE__ */ new Map();
630
+ this.lastSyncedId = 0n;
631
+ this.config = {
632
+ cronIntervalMs: config?.cronIntervalMs ?? 6e4,
633
+ // 1 minute
634
+ maxRetryAttempts: config?.maxRetryAttempts ?? 3,
635
+ syncPeriodMs: config?.syncPeriodMs ?? 3e3,
636
+ // 3 seconds
637
+ loadCommittedIntervals: config?.loadCommittedIntervals,
638
+ saveCommittedInterval: config?.saveCommittedInterval
639
+ };
640
+ this.getContainer = getContainer;
641
+ if (batchReaderAddress) {
642
+ this.batchReader = new import_contracts.SubscriptionBatchReaderContract(batchReaderAddress, provider);
643
+ console.log(`\u2713 SubscriptionBatchReader configured: ${batchReaderAddress}`);
644
+ } else {
645
+ console.warn("\u26A0\uFE0F SubscriptionBatchReader not configured - subscription sync disabled");
646
+ }
647
+ }
648
+ /**
649
+ * Start the scheduler service
650
+ */
651
+ start() {
652
+ console.log("\u{1F550} Starting Scheduler Service...");
653
+ console.log(` Commitment generation interval: ${this.config.cronIntervalMs}ms`);
654
+ console.log(` Sync period: ${this.config.syncPeriodMs}ms`);
655
+ if (this.config.loadCommittedIntervals) {
656
+ const loaded = this.config.loadCommittedIntervals();
657
+ for (const key of loaded) {
658
+ this.committedIntervals.add(key);
659
+ }
660
+ if (loaded.length > 0) {
661
+ console.log(` Loaded ${loaded.length} committed intervals from storage`);
662
+ }
663
+ }
664
+ this.intervalTimer = setInterval(() => this.generateCommitments(), this.config.cronIntervalMs);
665
+ this.syncTimer = setInterval(() => this.syncSubscriptions(), this.config.syncPeriodMs);
666
+ console.log("\u2713 Scheduler Service started");
667
+ }
668
+ /**
669
+ * Stop the scheduler service
670
+ */
671
+ stop() {
672
+ if (this.intervalTimer) {
673
+ clearInterval(this.intervalTimer);
674
+ this.intervalTimer = void 0;
675
+ }
676
+ if (this.syncTimer) {
677
+ clearInterval(this.syncTimer);
678
+ this.syncTimer = void 0;
679
+ }
680
+ console.log("\u2713 Scheduler Service stopped");
681
+ }
682
+ /**
683
+ * Track a new subscription
684
+ */
685
+ trackSubscription(subscription) {
686
+ const key = subscription.subscriptionId.toString();
687
+ if (this.subscriptions.has(key)) {
688
+ console.log(` Subscription ${key} already tracked, updating...`);
689
+ }
690
+ const now = Math.floor(Date.now() / 1e3);
691
+ const elapsed = now - Number(subscription.activeAt);
692
+ const currentInterval = subscription.intervalSeconds > 0n ? BigInt(Math.max(1, Math.floor(elapsed / Number(subscription.intervalSeconds)) + 1)) : 1n;
693
+ this.subscriptions.set(key, {
694
+ ...subscription,
695
+ currentInterval,
696
+ lastProcessedAt: Date.now(),
697
+ txAttempts: 0
698
+ });
699
+ console.log(`\u2713 Tracking subscription ${key}`);
700
+ this.emit("subscription:tracked", subscription.subscriptionId);
701
+ }
702
+ /**
703
+ * Remove a subscription from tracking
704
+ */
705
+ untrackSubscription(subscriptionId) {
706
+ const key = subscriptionId.toString();
707
+ if (this.subscriptions.delete(key)) {
708
+ const prefix = `${key}:`;
709
+ let cleanedCount = 0;
710
+ for (const commitmentKey of this.committedIntervals) {
711
+ if (commitmentKey.startsWith(prefix)) {
712
+ this.committedIntervals.delete(commitmentKey);
713
+ cleanedCount++;
714
+ }
715
+ }
716
+ if (cleanedCount > 0) {
717
+ console.log(` \u{1F9F9} Cleaned up ${cleanedCount} committed intervals for subscription ${key}`);
718
+ }
719
+ console.log(`\u2713 Stopped tracking subscription ${key}`);
720
+ this.emit("subscription:untracked", subscriptionId);
721
+ }
722
+ }
723
+ /**
724
+ * Mark an interval as committed (for RequestStarted events)
725
+ * Also persists to storage if callback is configured
726
+ */
727
+ markIntervalCommitted(subscriptionId, interval) {
728
+ const commitmentKey = `${subscriptionId}:${interval}`;
729
+ this.addCommittedInterval(commitmentKey);
730
+ console.log(` \u2713 Marked interval ${interval} as committed for subscription ${subscriptionId}`);
731
+ }
732
+ /**
733
+ * Add to committed intervals set and persist to storage
734
+ */
735
+ addCommittedInterval(key) {
736
+ if (!this.committedIntervals.has(key)) {
737
+ this.committedIntervals.add(key);
738
+ if (this.config.saveCommittedInterval) {
739
+ this.config.saveCommittedInterval(key);
740
+ }
741
+ }
742
+ }
743
+ /**
744
+ * Main commitment generation loop (runs every cronIntervalMs)
745
+ * Equivalent to Java's CommitmentGenerationService.generateCommitment()
746
+ */
747
+ async generateCommitments() {
748
+ try {
749
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
750
+ console.log(`
751
+ \u{1F504} [${timestamp}] Starting commitment generation task...`);
752
+ this.pruneFailedTxs();
753
+ await this.processActiveSubscriptions();
754
+ const endTimestamp = (/* @__PURE__ */ new Date()).toISOString();
755
+ console.log(`\u2713 [${endTimestamp}] Finished commitment generation task.
756
+ `);
757
+ } catch (error) {
758
+ console.error("\u274C Error in commitment generation:", error);
759
+ this.emit("error", error);
760
+ }
761
+ }
762
+ /**
763
+ * Process all active subscriptions
764
+ */
765
+ async processActiveSubscriptions() {
766
+ const latestBlock = await this.provider.getBlock("latest");
767
+ if (!latestBlock) {
768
+ console.warn(" Could not fetch latest block, skipping this cycle");
769
+ return;
770
+ }
771
+ const currentBlockTime = latestBlock.timestamp;
772
+ if (this.subscriptions.size === 0) {
773
+ console.log(" No subscriptions to process");
774
+ return;
775
+ }
776
+ console.log(` Processing ${this.subscriptions.size} subscription(s)...`);
777
+ for (const [subId, sub] of this.subscriptions.entries()) {
778
+ let currentInterval = 0n;
779
+ try {
780
+ if (sub.intervalSeconds <= 0n) {
781
+ console.warn(` Skipping subscription ${subId}: invalid intervalSeconds (${sub.intervalSeconds})`);
782
+ this.untrackSubscription(sub.subscriptionId);
783
+ continue;
784
+ }
785
+ try {
786
+ currentInterval = BigInt(await this.router.getComputeSubscriptionInterval(sub.subscriptionId));
787
+ } catch (error) {
788
+ console.warn(` Could not get interval from router for subscription ${subId}:`, error.message);
789
+ const intervalsSinceActive = BigInt(currentBlockTime) - sub.activeAt;
790
+ currentInterval = intervalsSinceActive / sub.intervalSeconds + 1n;
791
+ }
792
+ console.log(` Subscription ${subId}: currentInterval=${currentInterval}, maxExecutions=${sub.maxExecutions}, activeAt=${sub.activeAt}`);
793
+ if (!this.shouldProcess(sub, currentBlockTime)) {
794
+ continue;
795
+ }
796
+ const commitmentKey = `${subId}:${currentInterval}`;
797
+ if (this.committedIntervals.has(commitmentKey)) {
798
+ continue;
799
+ }
800
+ const hasCommitment = await this.hasRequestCommitments(sub.subscriptionId, currentInterval);
801
+ if (hasCommitment) {
802
+ this.addCommittedInterval(commitmentKey);
803
+ console.log(` Subscription ${subId} interval ${currentInterval} already committed`);
804
+ continue;
805
+ }
806
+ await this.prepareNextInterval(sub, currentInterval);
807
+ } catch (error) {
808
+ const errorMessage = error.message;
809
+ console.error(` Error processing subscription ${subId}:`, error);
810
+ const containsError = (ex, text) => {
811
+ let current = ex;
812
+ while (current) {
813
+ if (current.message?.includes(text)) return true;
814
+ current = current.cause;
815
+ }
816
+ return false;
817
+ };
818
+ if (containsError(error, "Panic due to OVERFLOW") || containsError(error, "arithmetic underflow or overflow")) {
819
+ console.log(` Interval ${currentInterval} for subscription ${subId} appears to be already executed (overflow), marking as committed`);
820
+ const commitmentKey = `${subId}:${currentInterval}`;
821
+ this.addCommittedInterval(commitmentKey);
822
+ sub.currentInterval = currentInterval + 1n;
823
+ } else if (containsError(error, "0x3cdc51d3") || containsError(error, "NoNextInterval")) {
824
+ console.log(` Subscription ${subId}: waiting for client to trigger interval 1 (NoNextInterval)`);
825
+ } else if (containsError(error, "execution reverted") || containsError(error, "Transaction simulation failed")) {
826
+ console.log(` Subscription ${subId} appears to be cancelled or invalid, untracking...`);
827
+ this.untrackSubscription(sub.subscriptionId);
828
+ }
829
+ }
830
+ }
831
+ }
832
+ /**
833
+ * Check if subscription should be processed
834
+ */
835
+ shouldProcess(sub, currentBlockTime) {
836
+ const subId = sub.subscriptionId.toString();
837
+ if (BigInt(currentBlockTime) < sub.activeAt) {
838
+ console.log(` Skip: not active yet (currentTime=${currentBlockTime}, activeAt=${sub.activeAt})`);
839
+ return false;
840
+ }
841
+ const intervalsSinceActive = BigInt(currentBlockTime) - sub.activeAt;
842
+ const currentInterval = intervalsSinceActive / sub.intervalSeconds + 1n;
843
+ if (sub.maxExecutions > 0n && currentInterval > sub.maxExecutions) {
844
+ console.log(` Subscription ${subId} completed (interval ${currentInterval} > maxExecutions ${sub.maxExecutions}), untracking...`);
845
+ this.untrackSubscription(sub.subscriptionId);
846
+ return false;
847
+ }
848
+ const runKey = `${sub.subscriptionId}:${currentInterval}`;
849
+ if (this.pendingTxs.has(runKey)) {
850
+ console.log(` Skip: pending transaction for interval ${currentInterval}`);
851
+ return false;
852
+ }
853
+ if (sub.txAttempts >= this.config.maxRetryAttempts) {
854
+ console.log(` Skip: max retry attempts reached (${sub.txAttempts}/${this.config.maxRetryAttempts})`);
855
+ return false;
856
+ }
857
+ return true;
858
+ }
859
+ /**
860
+ * Check if interval already has commitments on-chain
861
+ */
862
+ async hasRequestCommitments(subscriptionId, interval) {
863
+ try {
864
+ const redundancy = await this.coordinator.redundancyCount(
865
+ this.getRequestId(subscriptionId, interval)
866
+ );
867
+ return redundancy > 0;
868
+ } catch (error) {
869
+ console.error("Error checking commitments:", error);
870
+ return false;
871
+ }
872
+ }
873
+ /**
874
+ * Prepare next interval by calling coordinator contract
875
+ * Equivalent to Java's CoordinatorService.prepareNextInterval()
876
+ */
877
+ async prepareNextInterval(sub, interval) {
878
+ const runKey = `${sub.subscriptionId}:${interval}`;
879
+ try {
880
+ console.log(` Preparing interval ${interval} for subscription ${sub.subscriptionId}...`);
881
+ const currentIntervalNow = BigInt(await this.router.getComputeSubscriptionInterval(sub.subscriptionId));
882
+ if (currentIntervalNow !== interval && currentIntervalNow !== interval - 1n) {
883
+ console.log(` \u26A0\uFE0F Interval changed: expected ${interval}, blockchain is at ${currentIntervalNow}. Skipping.`);
884
+ return;
885
+ }
886
+ const tx = await this.coordinator.prepareNextInterval(
887
+ sub.subscriptionId,
888
+ interval,
889
+ this.agentWallet
890
+ );
891
+ this.pendingTxs.set(runKey, tx.hash);
892
+ sub.pendingTx = tx.hash;
893
+ console.log(` \u{1F4E4} Transaction sent: ${tx.hash}`);
894
+ const receipt = await tx.wait();
895
+ if (receipt.status === 1) {
896
+ console.log(
897
+ ` \u2713 Interval ${interval} prepared successfully (block ${receipt.blockNumber})`
898
+ );
899
+ const commitmentKey = `${sub.subscriptionId}:${interval}`;
900
+ this.addCommittedInterval(commitmentKey);
901
+ sub.currentInterval = interval;
902
+ sub.lastProcessedAt = Date.now();
903
+ sub.txAttempts = 0;
904
+ sub.pendingTx = void 0;
905
+ this.pendingTxs.delete(runKey);
906
+ const requestStartedEvent = this.parseRequestStartedFromReceipt(receipt, sub);
907
+ const gasUsed = receipt.gasUsed;
908
+ const gasPrice = receipt.gasPrice ?? tx.gasPrice ?? 0n;
909
+ const gasCost = gasUsed * gasPrice;
910
+ this.emit("commitment:success", {
911
+ subscriptionId: sub.subscriptionId,
912
+ interval,
913
+ txHash: tx.hash,
914
+ blockNumber: receipt.blockNumber,
915
+ gasUsed: gasUsed.toString(),
916
+ gasPrice: gasPrice.toString(),
917
+ gasCost: gasCost.toString(),
918
+ requestStartedEvent
919
+ // Include parsed event for immediate processing
920
+ });
921
+ } else {
922
+ throw new Error(`Transaction failed with status ${receipt.status}`);
923
+ }
924
+ } catch (error) {
925
+ console.error(` Failed to prepare interval for ${runKey}:`, error);
926
+ sub.pendingTx = void 0;
927
+ this.pendingTxs.delete(runKey);
928
+ const errorMessage = error.message || "";
929
+ const isNoNextIntervalError = errorMessage.includes("0x3cdc51d3") || errorMessage.includes("NoNextInterval");
930
+ if (isNoNextIntervalError) {
931
+ console.log(` Subscription ${sub.subscriptionId}: NoNextInterval - waiting for client to trigger interval 1`);
932
+ sub.txAttempts = 0;
933
+ return;
934
+ }
935
+ sub.txAttempts++;
936
+ if (sub.txAttempts >= this.config.maxRetryAttempts) {
937
+ console.log(` Max retry attempts reached for ${runKey}`);
938
+ this.emit("commitment:failed", {
939
+ subscriptionId: sub.subscriptionId,
940
+ interval,
941
+ error
942
+ });
943
+ }
944
+ }
945
+ }
946
+ /**
947
+ * Prune transactions that have failed
948
+ */
949
+ pruneFailedTxs() {
950
+ const fiveMinutesAgo = Date.now() - 5 * 60 * 1e3;
951
+ for (const [subId, sub] of this.subscriptions.entries()) {
952
+ if (sub.lastProcessedAt < fiveMinutesAgo && sub.pendingTx) {
953
+ console.log(` Pruning stale transaction for subscription ${subId}`);
954
+ sub.pendingTx = void 0;
955
+ sub.txAttempts = 0;
956
+ }
957
+ }
958
+ }
959
+ /**
960
+ * Sync subscriptions (placeholder for blockchain event listening)
961
+ */
962
+ /**
963
+ * Sync subscriptions from blockchain
964
+ * Reads subscriptions in batches and tracks active ones
965
+ */
966
+ async syncSubscriptions() {
967
+ if (!this.batchReader) {
968
+ this.emit("sync:tick");
969
+ return;
970
+ }
971
+ try {
972
+ if (this.maxSubscriptionId === void 0) {
973
+ this.maxSubscriptionId = await this.router.getLastSubscriptionId();
974
+ console.log(`\u{1F4CA} Total subscriptions in registry: ${this.maxSubscriptionId}`);
975
+ }
976
+ if (this.lastSyncedId >= this.maxSubscriptionId) {
977
+ const latestMaxId = await this.router.getLastSubscriptionId();
978
+ if (latestMaxId > this.maxSubscriptionId) {
979
+ console.log(`\u{1F4CA} Found new subscriptions: ${latestMaxId} (was ${this.maxSubscriptionId})`);
980
+ this.maxSubscriptionId = latestMaxId;
981
+ } else {
982
+ this.emit("sync:tick");
983
+ return;
984
+ }
985
+ }
986
+ const maxSubId = this.maxSubscriptionId;
987
+ const currentBlock = await this.provider.getBlockNumber();
988
+ const block = await this.provider.getBlock(currentBlock);
989
+ const blockTime = block?.timestamp || Math.floor(Date.now() / 1e3);
990
+ const BATCH_SIZE = 100n;
991
+ const startId = this.lastSyncedId + 1n;
992
+ const endId = startId + BATCH_SIZE - 1n > maxSubId ? maxSubId : startId + BATCH_SIZE - 1n;
993
+ const subscriptions = await this.batchReader.getSubscriptions(startId, endId, currentBlock);
994
+ if (subscriptions.length === 0) {
995
+ this.emit("sync:tick");
996
+ return;
997
+ }
998
+ let newSubscriptions = 0;
999
+ let skippedContainers = 0;
1000
+ let skippedInactive = 0;
1001
+ let skippedEmpty = 0;
1002
+ let skippedOnDemand = 0;
1003
+ console.log(` Syncing ${subscriptions.length} subscriptions (blockTime: ${blockTime})`);
1004
+ for (let i = 0; i < subscriptions.length; i++) {
1005
+ const sub = subscriptions[i];
1006
+ const subscriptionId = startId + BigInt(i);
1007
+ if (sub.containerId === "0x0000000000000000000000000000000000000000000000000000000000000000") {
1008
+ skippedEmpty++;
1009
+ continue;
1010
+ }
1011
+ if (!this.isSubscriptionActive(sub, blockTime)) {
1012
+ if (sub.intervalSeconds > 0) {
1013
+ console.log(` Sub ${subscriptionId}: inactive (activeAt=${sub.activeAt}, now=${blockTime})`);
1014
+ }
1015
+ skippedInactive++;
1016
+ continue;
1017
+ }
1018
+ if (this.getContainer && !this.getContainer(sub.containerId)) {
1019
+ skippedContainers++;
1020
+ continue;
1021
+ }
1022
+ if (this.trackSubscriptionFromConfig(sub, subscriptionId)) {
1023
+ newSubscriptions++;
1024
+ } else {
1025
+ if (sub.intervalSeconds <= 0) {
1026
+ skippedOnDemand++;
1027
+ }
1028
+ }
1029
+ }
1030
+ console.log(` Sync stats: ${newSubscriptions} tracked, ${skippedEmpty} empty, ${skippedInactive} inactive, ${skippedContainers} unsupported containers, ${skippedOnDemand} on-demand`);
1031
+ this.lastSyncedId = endId;
1032
+ if (newSubscriptions > 0) {
1033
+ console.log(`\u2713 Synced ${newSubscriptions} active subscriptions (ID ${startId} - ${endId})`);
1034
+ }
1035
+ if (this.lastSyncedId >= maxSubId) {
1036
+ console.log(`\u2713 Sync completed - processed all ${maxSubId} subscriptions`);
1037
+ }
1038
+ this.emit("sync:completed", {
1039
+ subscriptions: this.subscriptions.size,
1040
+ newSubscriptions
1041
+ });
1042
+ } catch (error) {
1043
+ console.error("Error syncing subscriptions:", error);
1044
+ this.emit("sync:error", error);
1045
+ }
1046
+ }
1047
+ /**
1048
+ * Check if subscription is currently active
1049
+ */
1050
+ isSubscriptionActive(sub, currentBlockTime) {
1051
+ if (currentBlockTime < sub.activeAt) {
1052
+ return false;
1053
+ }
1054
+ if (sub.maxExecutions > 0 && sub.intervalSeconds > 0) {
1055
+ const elapsed = currentBlockTime - sub.activeAt;
1056
+ const currentInterval = Math.floor(elapsed / sub.intervalSeconds);
1057
+ if (currentInterval >= sub.maxExecutions) {
1058
+ return false;
1059
+ }
1060
+ }
1061
+ return true;
1062
+ }
1063
+ /**
1064
+ * Track subscription from ComputeSubscription config
1065
+ * Returns true if subscription was tracked, false if skipped
1066
+ */
1067
+ trackSubscriptionFromConfig(sub, subscriptionId) {
1068
+ if (sub.containerId === "0x0000000000000000000000000000000000000000000000000000000000000000") {
1069
+ return false;
1070
+ }
1071
+ if (sub.client === "0x0000000000000000000000000000000000000000") {
1072
+ return false;
1073
+ }
1074
+ const key = subscriptionId.toString();
1075
+ if (this.subscriptions.has(key)) {
1076
+ return false;
1077
+ }
1078
+ if (sub.intervalSeconds <= 0) {
1079
+ return false;
1080
+ }
1081
+ try {
1082
+ const { ethers: ethers6 } = require("ethers");
1083
+ const containerIdStr = ethers6.decodeBytes32String(sub.containerId);
1084
+ console.log(` \u2713 Tracking subscription: ${containerIdStr}`);
1085
+ console.log(` Client: ${sub.client}`);
1086
+ console.log(` Interval: ${sub.intervalSeconds}s`);
1087
+ console.log(` Max Executions: ${sub.maxExecutions}`);
1088
+ } catch (e) {
1089
+ console.log(` \u2713 Tracking subscription: ${sub.containerId}`);
1090
+ }
1091
+ const now = Math.floor(Date.now() / 1e3);
1092
+ const elapsed = now - sub.activeAt;
1093
+ const currentInterval = sub.intervalSeconds > 0 ? Math.max(1, Math.floor(elapsed / sub.intervalSeconds) + 1) : 1;
1094
+ this.subscriptions.set(key, {
1095
+ subscriptionId,
1096
+ routeId: sub.routeId,
1097
+ containerId: sub.containerId,
1098
+ client: sub.client,
1099
+ wallet: sub.wallet,
1100
+ activeAt: BigInt(sub.activeAt),
1101
+ intervalSeconds: BigInt(sub.intervalSeconds),
1102
+ maxExecutions: Number.isFinite(sub.maxExecutions) ? BigInt(sub.maxExecutions) : 0n,
1103
+ redundancy: sub.redundancy,
1104
+ verifier: sub.verifier || void 0,
1105
+ currentInterval: BigInt(currentInterval),
1106
+ lastProcessedAt: Date.now(),
1107
+ txAttempts: 0
1108
+ });
1109
+ return true;
1110
+ }
1111
+ /**
1112
+ * Get request ID (hash of subscription ID and interval)
1113
+ */
1114
+ getRequestId(subscriptionId, interval) {
1115
+ const ethers6 = require("ethers");
1116
+ const abiCoder = ethers6.AbiCoder.defaultAbiCoder();
1117
+ return ethers6.keccak256(abiCoder.encode(["uint256", "uint256"], [subscriptionId, interval]));
1118
+ }
1119
+ /**
1120
+ * Get scheduler statistics
1121
+ */
1122
+ getStats() {
1123
+ const now = Math.floor(Date.now() / 1e3);
1124
+ const activeCount = Array.from(this.subscriptions.values()).filter((sub) => {
1125
+ if (BigInt(now) < sub.activeAt) {
1126
+ return false;
1127
+ }
1128
+ if (sub.maxExecutions > 0n) {
1129
+ const elapsed = BigInt(now) - sub.activeAt;
1130
+ const currentInterval = elapsed / sub.intervalSeconds;
1131
+ if (currentInterval >= sub.maxExecutions) {
1132
+ return false;
1133
+ }
1134
+ }
1135
+ return true;
1136
+ }).length;
1137
+ return {
1138
+ totalSubscriptions: this.subscriptions.size,
1139
+ activeSubscriptions: activeCount,
1140
+ committedIntervals: this.committedIntervals.size,
1141
+ pendingTransactions: this.pendingTxs.size
1142
+ };
1143
+ }
1144
+ /**
1145
+ * Get all tracked subscriptions
1146
+ */
1147
+ getSubscriptions() {
1148
+ return Array.from(this.subscriptions.values());
1149
+ }
1150
+ /**
1151
+ * Parse RequestStarted event from transaction receipt
1152
+ * This allows the agent to process the event immediately without waiting for WebSocket
1153
+ */
1154
+ parseRequestStartedFromReceipt(receipt, sub) {
1155
+ try {
1156
+ for (const log of receipt.logs) {
1157
+ try {
1158
+ const parsed = this.coordinator.interface.parseLog({
1159
+ topics: log.topics,
1160
+ data: log.data
1161
+ });
1162
+ if (parsed && parsed.name === "RequestStarted") {
1163
+ const commitment = parsed.args.commitment;
1164
+ return {
1165
+ requestId: parsed.args.requestId,
1166
+ subscriptionId: parsed.args.subscriptionId,
1167
+ containerId: parsed.args.containerId,
1168
+ interval: Number(commitment.interval),
1169
+ redundancy: Number(commitment.redundancy),
1170
+ useDeliveryInbox: commitment.useDeliveryInbox,
1171
+ feeAmount: commitment.feeAmount,
1172
+ feeToken: commitment.feeToken,
1173
+ verifier: commitment.verifier,
1174
+ coordinator: commitment.coordinator,
1175
+ walletAddress: commitment.walletAddress,
1176
+ blockNumber: receipt.blockNumber
1177
+ };
1178
+ }
1179
+ } catch {
1180
+ }
1181
+ }
1182
+ } catch (error) {
1183
+ console.warn(" \u26A0\uFE0F Could not parse RequestStarted event from receipt:", error);
1184
+ }
1185
+ return null;
1186
+ }
1187
+ };
1188
+
1189
+ // src/NoosphereAgent.ts
1190
+ var import_crypto = require("@noosphere/crypto");
1191
+ var import_registry = require("@noosphere/registry");
1192
+ var import_contracts2 = require("@noosphere/contracts");
1193
+
1194
+ // src/utils/CommitmentUtils.ts
1195
+ var import_ethers2 = require("ethers");
1196
+ var CommitmentUtils = class {
1197
+ /**
1198
+ * Calculate commitment hash
1199
+ * Matches the keccak256(abi.encode(commitment)) in Solidity
1200
+ */
1201
+ static hash(commitment) {
1202
+ const encoded = import_ethers2.ethers.AbiCoder.defaultAbiCoder().encode(
1203
+ [
1204
+ "bytes32",
1205
+ // requestId
1206
+ "uint64",
1207
+ // subscriptionId
1208
+ "bytes32",
1209
+ // containerId
1210
+ "uint32",
1211
+ // interval
1212
+ "bool",
1213
+ // useDeliveryInbox
1214
+ "uint16",
1215
+ // redundancy
1216
+ "address",
1217
+ // walletAddress
1218
+ "uint256",
1219
+ // feeAmount
1220
+ "address",
1221
+ // feeToken
1222
+ "address",
1223
+ // verifier
1224
+ "address"
1225
+ // coordinator
1226
+ ],
1227
+ [
1228
+ commitment.requestId,
1229
+ commitment.subscriptionId,
1230
+ commitment.containerId,
1231
+ commitment.interval,
1232
+ commitment.useDeliveryInbox,
1233
+ commitment.redundancy,
1234
+ commitment.walletAddress,
1235
+ commitment.feeAmount,
1236
+ commitment.feeToken,
1237
+ commitment.verifier,
1238
+ commitment.coordinator
1239
+ ]
1240
+ );
1241
+ return import_ethers2.ethers.keccak256(encoded);
1242
+ }
1243
+ /**
1244
+ * Verify commitment hash matches expected value
1245
+ */
1246
+ static verify(commitment, expectedHash) {
1247
+ const actualHash = this.hash(commitment);
1248
+ return actualHash === expectedHash;
1249
+ }
1250
+ /**
1251
+ * Encode commitment data for reportComputeResult
1252
+ * Returns ABI-encoded commitment struct
1253
+ */
1254
+ static encode(commitment) {
1255
+ return import_ethers2.ethers.AbiCoder.defaultAbiCoder().encode(
1256
+ [
1257
+ "bytes32",
1258
+ // requestId
1259
+ "uint64",
1260
+ // subscriptionId
1261
+ "bytes32",
1262
+ // containerId
1263
+ "uint32",
1264
+ // interval
1265
+ "bool",
1266
+ // useDeliveryInbox
1267
+ "uint16",
1268
+ // redundancy
1269
+ "address",
1270
+ // walletAddress
1271
+ "uint256",
1272
+ // feeAmount
1273
+ "address",
1274
+ // feeToken
1275
+ "address",
1276
+ // verifier
1277
+ "address"
1278
+ // coordinator
1279
+ ],
1280
+ [
1281
+ commitment.requestId,
1282
+ commitment.subscriptionId,
1283
+ commitment.containerId,
1284
+ commitment.interval,
1285
+ commitment.useDeliveryInbox,
1286
+ commitment.redundancy,
1287
+ commitment.walletAddress,
1288
+ commitment.feeAmount,
1289
+ commitment.feeToken,
1290
+ commitment.verifier,
1291
+ commitment.coordinator
1292
+ ]
1293
+ );
1294
+ }
1295
+ /**
1296
+ * Create Commitment from RequestStartedEvent
1297
+ */
1298
+ static fromEvent(event, walletAddress) {
1299
+ return {
1300
+ requestId: event.requestId,
1301
+ subscriptionId: event.subscriptionId,
1302
+ containerId: event.containerId,
1303
+ interval: event.interval,
1304
+ redundancy: event.redundancy,
1305
+ useDeliveryInbox: event.useDeliveryInbox || false,
1306
+ feeToken: event.feeToken,
1307
+ feeAmount: event.feeAmount,
1308
+ walletAddress,
1309
+ // Client wallet from subscription
1310
+ verifier: event.verifier || import_ethers2.ethers.ZeroAddress,
1311
+ coordinator: event.coordinator
1312
+ };
1313
+ }
1314
+ };
1315
+
1316
+ // src/utils/ConfigLoader.ts
1317
+ var import_fs = require("fs");
1318
+ var import_ethers3 = require("ethers");
1319
+ var ConfigLoader = class {
1320
+ /**
1321
+ * Load agent configuration from JSON file
1322
+ */
1323
+ static loadFromFile(configPath) {
1324
+ try {
1325
+ const configData = (0, import_fs.readFileSync)(configPath, "utf-8");
1326
+ const config = JSON.parse(configData);
1327
+ if (!config.chain || !config.chain.enabled) {
1328
+ throw new Error("Chain configuration is required and must be enabled");
1329
+ }
1330
+ if (!config.chain.rpcUrl) {
1331
+ throw new Error("Chain RPC URL is required");
1332
+ }
1333
+ if (!config.chain.routerAddress) {
1334
+ throw new Error("Router address is required");
1335
+ }
1336
+ if (!config.containers || config.containers.length === 0) {
1337
+ console.warn("\u26A0\uFE0F No containers configured in config file");
1338
+ }
1339
+ return config;
1340
+ } catch (error) {
1341
+ if (error.code === "ENOENT") {
1342
+ throw new Error(`Config file not found: ${configPath}`);
1343
+ }
1344
+ throw error;
1345
+ }
1346
+ }
1347
+ /**
1348
+ * Convert ContainerConfig to ContainerMetadata format
1349
+ */
1350
+ static containerConfigToMetadata(containerConfig) {
1351
+ const [imageName, tag] = containerConfig.image.includes(":") ? containerConfig.image.split(":") : [containerConfig.image, "latest"];
1352
+ const name = imageName.split("/").pop() || containerConfig.id;
1353
+ let payments;
1354
+ if (containerConfig.acceptedPayments) {
1355
+ const basePrice = Object.values(containerConfig.acceptedPayments)[0]?.toString() || "0";
1356
+ payments = {
1357
+ basePrice,
1358
+ unit: "wei",
1359
+ per: "execution"
1360
+ };
1361
+ }
1362
+ return {
1363
+ id: containerConfig.id,
1364
+ name,
1365
+ image: imageName,
1366
+ tag,
1367
+ port: containerConfig.port,
1368
+ env: containerConfig.env,
1369
+ verified: !!containerConfig.verifierAddress,
1370
+ payments
1371
+ };
1372
+ }
1373
+ /**
1374
+ * Get all containers from config as ContainerMetadata array
1375
+ */
1376
+ static getContainersFromConfig(config) {
1377
+ const containersMap = /* @__PURE__ */ new Map();
1378
+ for (const containerConfig of config.containers) {
1379
+ const metadata = this.containerConfigToMetadata(containerConfig);
1380
+ const containerIdHash = import_ethers3.ethers.keccak256(
1381
+ import_ethers3.ethers.AbiCoder.defaultAbiCoder().encode(["string"], [containerConfig.id])
1382
+ );
1383
+ containersMap.set(containerIdHash, metadata);
1384
+ }
1385
+ return containersMap;
1386
+ }
1387
+ /**
1388
+ * Get container config by ID
1389
+ */
1390
+ static getContainerConfig(config, containerId) {
1391
+ return config.containers.find((c) => c.id === containerId);
1392
+ }
1393
+ };
1394
+
1395
+ // src/NoosphereAgent.ts
1396
+ var NoosphereAgent = class _NoosphereAgent {
1397
+ // Deduplication: track requests being processed
1398
+ constructor(options) {
1399
+ this.options = options;
1400
+ this.isRunning = false;
1401
+ this.processingRequests = /* @__PURE__ */ new Set();
1402
+ this.config = options.config;
1403
+ this.provider = new import_ethers4.ethers.JsonRpcProvider(options.config.rpcUrl);
1404
+ const provider = this.provider;
1405
+ const routerAbi = options.routerAbi || import_contracts2.ABIs.Router;
1406
+ const coordinatorAbi = options.coordinatorAbi || import_contracts2.ABIs.Coordinator;
1407
+ if (options.walletManager) {
1408
+ this.walletManager = options.walletManager;
1409
+ } else if (options.config.privateKey) {
1410
+ this.walletManager = new import_crypto.WalletManager(options.config.privateKey, provider);
1411
+ } else {
1412
+ throw new Error(
1413
+ "Either walletManager or config.privateKey must be provided. Recommended: Use NoosphereAgent.fromKeystore() for production."
1414
+ );
1415
+ }
1416
+ this.containerManager = new ContainerManager();
1417
+ this.registryManager = new import_registry.RegistryManager({
1418
+ autoSync: true,
1419
+ // Enable automatic sync with remote registry
1420
+ cacheTTL: 36e5
1421
+ // 1 hour cache
1422
+ });
1423
+ this.eventMonitor = new EventMonitor(options.config, routerAbi, coordinatorAbi, {
1424
+ loadCheckpoint: options.loadCheckpoint,
1425
+ saveCheckpoint: options.saveCheckpoint
1426
+ });
1427
+ this.router = new import_ethers4.ethers.Contract(
1428
+ options.config.routerAddress,
1429
+ routerAbi,
1430
+ this.provider
1431
+ );
1432
+ this.coordinator = new import_ethers4.ethers.Contract(
1433
+ options.config.coordinatorAddress,
1434
+ coordinatorAbi,
1435
+ this.walletManager.getWallet()
1436
+ );
1437
+ this.getContainer = options.getContainer;
1438
+ this.containers = options.containers;
1439
+ this.scheduler = new SchedulerService(
1440
+ provider,
1441
+ this.router,
1442
+ this.coordinator,
1443
+ this.walletManager.getAddress(),
1444
+ void 0,
1445
+ // batchReaderAddress (set later)
1446
+ void 0,
1447
+ // config (set later)
1448
+ this.getContainer
1449
+ // Pass container filter
1450
+ );
1451
+ this.paymentWallet = options.paymentWallet;
1452
+ if (!this.getContainer && (!this.containers || this.containers.size === 0)) {
1453
+ console.warn("\u26A0\uFE0F No container source provided. Agent will not be able to execute requests.");
1454
+ }
1455
+ }
1456
+ /**
1457
+ * Initialize NoosphereAgent from config.json (RECOMMENDED)
1458
+ * This loads all configuration including containers from a config file
1459
+ *
1460
+ * @param configPath - Path to config.json file
1461
+ * @param routerAbi - Router contract ABI (optional - defaults to ABIs.Router)
1462
+ * @param coordinatorAbi - Coordinator contract ABI (optional - defaults to ABIs.Coordinator)
1463
+ * @returns Initialized NoosphereAgent
1464
+ */
1465
+ static async fromConfig(configPath, routerAbi, coordinatorAbi) {
1466
+ const fullConfig = ConfigLoader.loadFromFile(configPath);
1467
+ const keystorePath = fullConfig.chain.wallet.keystore?.path;
1468
+ const password = fullConfig.chain.wallet.keystore?.password;
1469
+ if (!keystorePath || !password) {
1470
+ throw new Error("Keystore path and password are required in config.chain.wallet.keystore");
1471
+ }
1472
+ const containers = ConfigLoader.getContainersFromConfig(fullConfig);
1473
+ console.log(`\u{1F4E6} Loaded ${containers.size} containers from config:`);
1474
+ for (const [id, container] of containers.entries()) {
1475
+ console.log(` - ${id}: ${container.image}:${container.tag || "latest"}`);
1476
+ }
1477
+ const provider = new import_ethers4.ethers.JsonRpcProvider(fullConfig.chain.rpcUrl);
1478
+ const keystoreManager = new import_crypto.KeystoreManager(keystorePath, password);
1479
+ await keystoreManager.load();
1480
+ const walletManager = await import_crypto.WalletManager.fromKeystoreManager(keystoreManager, provider);
1481
+ const agentConfig = {
1482
+ rpcUrl: fullConfig.chain.rpcUrl,
1483
+ wsRpcUrl: fullConfig.chain.wsRpcUrl,
1484
+ routerAddress: fullConfig.chain.routerAddress,
1485
+ coordinatorAddress: fullConfig.chain.coordinatorAddress || fullConfig.chain.routerAddress,
1486
+ deploymentBlock: fullConfig.chain.deploymentBlock,
1487
+ pollingInterval: fullConfig.chain.processingInterval
1488
+ };
1489
+ const paymentWallet = fullConfig.chain.wallet.paymentAddress;
1490
+ return new _NoosphereAgent({
1491
+ config: agentConfig,
1492
+ routerAbi,
1493
+ coordinatorAbi,
1494
+ walletManager,
1495
+ containers,
1496
+ paymentWallet
1497
+ });
1498
+ }
1499
+ /**
1500
+ * Initialize NoosphereAgent from keystore (RECOMMENDED)
1501
+ * This is the secure way to initialize an agent in production
1502
+ *
1503
+ * @param keystorePath - Path to keystore file
1504
+ * @param password - Keystore password
1505
+ * @param options - Agent configuration options
1506
+ * @returns Initialized NoosphereAgent
1507
+ */
1508
+ static async fromKeystore(keystorePath, password, options) {
1509
+ const provider = new import_ethers4.ethers.JsonRpcProvider(options.config.rpcUrl);
1510
+ const keystoreManager = new import_crypto.KeystoreManager(keystorePath, password);
1511
+ await keystoreManager.load();
1512
+ const walletManager = await import_crypto.WalletManager.fromKeystoreManager(keystoreManager, provider);
1513
+ return new _NoosphereAgent({
1514
+ ...options,
1515
+ walletManager
1516
+ });
1517
+ }
1518
+ async start() {
1519
+ console.log("Starting Noosphere Agent...");
1520
+ console.log("\u{1F4CB} Loading container registry...");
1521
+ await this.registryManager.load();
1522
+ const stats = this.registryManager.getStats();
1523
+ console.log(
1524
+ `\u2713 Registry loaded: ${stats.totalContainers} containers, ${stats.totalVerifiers} verifiers`
1525
+ );
1526
+ const dockerAvailable = await this.containerManager.checkDockerAvailable();
1527
+ if (!dockerAvailable) {
1528
+ throw new Error("Docker is not available. Please ensure Docker daemon is running.");
1529
+ }
1530
+ const address = this.walletManager.getAddress();
1531
+ const balance = await this.walletManager.getBalance();
1532
+ console.log(`Agent wallet: ${address}`);
1533
+ console.log(`Balance: ${import_ethers4.ethers.formatEther(balance)} ETH`);
1534
+ if (balance === 0n) {
1535
+ console.warn("\u26A0\uFE0F Warning: Wallet has zero balance. Agent needs ETH for gas fees.");
1536
+ }
1537
+ if (this.containers && this.containers.size > 0) {
1538
+ console.log(`
1539
+ \u{1F680} Preparing ${this.containers.size} containers...`);
1540
+ await this.containerManager.prepareContainers(this.containers);
1541
+ }
1542
+ await this.eventMonitor.connect();
1543
+ this.eventMonitor.on("RequestStarted", async (event) => {
1544
+ await this.handleRequest(event);
1545
+ });
1546
+ await this.eventMonitor.start();
1547
+ try {
1548
+ const batchReaderAddress = await this.coordinator.getSubscriptionBatchReader();
1549
+ if (batchReaderAddress && batchReaderAddress !== "0x0000000000000000000000000000000000000000") {
1550
+ console.log(`\u2713 SubscriptionBatchReader found: ${batchReaderAddress}`);
1551
+ this.scheduler.stop();
1552
+ this.scheduler = new SchedulerService(
1553
+ this.provider,
1554
+ this.router,
1555
+ this.coordinator,
1556
+ this.walletManager.getAddress(),
1557
+ batchReaderAddress,
1558
+ this.options.schedulerConfig || {
1559
+ cronIntervalMs: 6e4,
1560
+ // 1 minute (default)
1561
+ syncPeriodMs: 3e3,
1562
+ // 3 seconds (default)
1563
+ maxRetryAttempts: 3
1564
+ // 3 retries (default)
1565
+ },
1566
+ this.getContainer
1567
+ // Pass container filter
1568
+ );
1569
+ } else {
1570
+ console.warn("\u26A0\uFE0F SubscriptionBatchReader not available - subscription sync disabled");
1571
+ }
1572
+ } catch (error) {
1573
+ console.warn("\u26A0\uFE0F Failed to get SubscriptionBatchReader address:", error.message);
1574
+ }
1575
+ this.scheduler.start();
1576
+ this.scheduler.on("commitment:success", async (data) => {
1577
+ if (this.options.onCommitmentSuccess) {
1578
+ this.options.onCommitmentSuccess({
1579
+ subscriptionId: data.subscriptionId,
1580
+ interval: data.interval,
1581
+ txHash: data.txHash,
1582
+ blockNumber: data.blockNumber,
1583
+ gasUsed: data.gasUsed || "0",
1584
+ gasPrice: data.gasPrice || "0",
1585
+ gasCost: data.gasCost || "0"
1586
+ });
1587
+ }
1588
+ if (data.requestStartedEvent) {
1589
+ console.log(` \u{1F4E5} Processing RequestStarted from prepare receipt (fallback for missed WebSocket)`);
1590
+ await this.handleRequest(data.requestStartedEvent);
1591
+ }
1592
+ });
1593
+ this.isRunning = true;
1594
+ console.log("\u2713 Noosphere Agent is running");
1595
+ console.log("Listening for requests...");
1596
+ }
1597
+ /**
1598
+ * Convert registry ContainerMetadata to agent-core ContainerMetadata
1599
+ */
1600
+ convertRegistryContainer(registryContainer) {
1601
+ const [image, tag] = registryContainer.imageName.split(":");
1602
+ return {
1603
+ id: registryContainer.id,
1604
+ name: registryContainer.name,
1605
+ image,
1606
+ tag: tag || "latest",
1607
+ port: registryContainer.port?.toString(),
1608
+ env: registryContainer.env,
1609
+ requirements: registryContainer.requirements,
1610
+ payments: registryContainer.payments ? {
1611
+ basePrice: registryContainer.payments.basePrice,
1612
+ unit: registryContainer.payments.token,
1613
+ per: registryContainer.payments.per
1614
+ } : void 0,
1615
+ verified: registryContainer.verified
1616
+ };
1617
+ }
1618
+ async handleRequest(event) {
1619
+ const requestIdShort = event.requestId.slice(0, 10);
1620
+ if (this.processingRequests.has(event.requestId)) {
1621
+ console.log(` \u23ED\uFE0F Request ${requestIdShort}... already being processed, skipping duplicate`);
1622
+ return;
1623
+ }
1624
+ if (this.options.isRequestProcessed && this.options.isRequestProcessed(event.requestId)) {
1625
+ console.log(` \u23ED\uFE0F Request ${requestIdShort}... already processed, skipping`);
1626
+ return;
1627
+ }
1628
+ this.processingRequests.add(event.requestId);
1629
+ console.log(`
1630
+ [${(/* @__PURE__ */ new Date()).toISOString()}] RequestStarted: ${requestIdShort}...`);
1631
+ console.log(` SubscriptionId: ${event.subscriptionId}`);
1632
+ console.log(` Interval: ${event.interval}`);
1633
+ console.log(` ContainerId: ${event.containerId.slice(0, 10)}...`);
1634
+ if (this.options.onRequestStarted) {
1635
+ this.options.onRequestStarted({
1636
+ requestId: event.requestId,
1637
+ subscriptionId: Number(event.subscriptionId),
1638
+ interval: Number(event.interval),
1639
+ containerId: event.containerId,
1640
+ redundancy: event.redundancy,
1641
+ feeAmount: event.feeAmount.toString(),
1642
+ feeToken: event.feeToken,
1643
+ verifier: event.verifier,
1644
+ walletAddress: event.walletAddress,
1645
+ blockNumber: event.blockNumber
1646
+ });
1647
+ }
1648
+ try {
1649
+ const currentInterval = await this.router.getComputeSubscriptionInterval(event.subscriptionId);
1650
+ const eventInterval = Number(event.interval);
1651
+ const isOneTimeExecution = currentInterval === 4294967295n;
1652
+ if (!isOneTimeExecution && currentInterval > eventInterval + 2) {
1653
+ console.log(` \u23ED\uFE0F Skipping old interval ${eventInterval} (current: ${currentInterval})`);
1654
+ if (this.options.onRequestSkipped) {
1655
+ this.options.onRequestSkipped(event.requestId, `Old interval ${eventInterval} (current: ${currentInterval})`);
1656
+ }
1657
+ this.processingRequests.delete(event.requestId);
1658
+ return;
1659
+ }
1660
+ } catch (error) {
1661
+ console.warn(` Could not verify interval currency:`, error.message);
1662
+ }
1663
+ this.scheduler.markIntervalCommitted(BigInt(event.subscriptionId), BigInt(event.interval));
1664
+ try {
1665
+ await this.waitForPriority(event);
1666
+ if (this.options.onRequestProcessing) {
1667
+ this.options.onRequestProcessing(event.requestId);
1668
+ }
1669
+ const currentCount = await this.coordinator.redundancyCount(event.requestId);
1670
+ if (currentCount >= event.redundancy) {
1671
+ console.log(` \u23ED\uFE0F Already fulfilled (${currentCount}/${event.redundancy}), skipping`);
1672
+ if (this.options.onRequestSkipped) {
1673
+ this.options.onRequestSkipped(event.requestId, `Already fulfilled (${currentCount}/${event.redundancy})`);
1674
+ }
1675
+ this.processingRequests.delete(event.requestId);
1676
+ return;
1677
+ }
1678
+ let container;
1679
+ if (this.getContainer) {
1680
+ container = this.getContainer(event.containerId);
1681
+ if (container) {
1682
+ console.log(` \u{1F4E6} Container found via callback: ${container.name}`);
1683
+ }
1684
+ }
1685
+ if (!container) {
1686
+ const registryContainer = this.registryManager.getContainer(event.containerId);
1687
+ if (registryContainer) {
1688
+ console.log(` \u{1F4CB} Container found in registry: ${registryContainer.name}`);
1689
+ container = this.convertRegistryContainer(registryContainer);
1690
+ }
1691
+ }
1692
+ if (!container && this.containers) {
1693
+ container = this.containers.get(event.containerId);
1694
+ if (container) {
1695
+ console.log(` \u{1F4E6} Container found in config: ${container.name}`);
1696
+ }
1697
+ }
1698
+ if (!container) {
1699
+ console.error(` \u274C Container not found: ${event.containerId}`);
1700
+ console.error(` \u{1F4A1} Try adding it to the registry or config file`);
1701
+ if (this.options.onRequestSkipped) {
1702
+ this.options.onRequestSkipped(event.requestId, `Container not found: ${event.containerId}`);
1703
+ }
1704
+ return;
1705
+ }
1706
+ console.log(
1707
+ ` \u{1F4E6} Using container: ${container.name} (${container.image}:${container.tag || "latest"})`
1708
+ );
1709
+ const subscription = await this.router.getComputeSubscription(event.subscriptionId);
1710
+ const clientAddress = subscription.client;
1711
+ if (!clientAddress || clientAddress === "0x0000000000000000000000000000000000000000") {
1712
+ console.error(` \u274C Invalid client address for subscription ${event.subscriptionId}`);
1713
+ if (this.options.onRequestFailed) {
1714
+ this.options.onRequestFailed(event.requestId, `Invalid client address for subscription ${event.subscriptionId}`);
1715
+ }
1716
+ return;
1717
+ }
1718
+ console.log(` \u{1F4DE} Fetching inputs from client: ${clientAddress.slice(0, 10)}...`);
1719
+ const clientAbi = [
1720
+ "function getComputeInputs(uint64 subscriptionId, uint32 interval, uint32 timestamp, address caller) external view returns (bytes memory)"
1721
+ ];
1722
+ const client = new import_ethers4.ethers.Contract(clientAddress, clientAbi, this.provider);
1723
+ const timestamp = Math.floor(Date.now() / 1e3);
1724
+ let inputBytes;
1725
+ try {
1726
+ inputBytes = await client.getComputeInputs(
1727
+ event.subscriptionId,
1728
+ event.interval,
1729
+ timestamp,
1730
+ this.walletManager.getAddress()
1731
+ );
1732
+ } catch (error) {
1733
+ const errorMessage = error.message || String(error);
1734
+ console.error(` \u274C Failed to get inputs from client:`, error);
1735
+ if (this.options.onRequestFailed) {
1736
+ this.options.onRequestFailed(event.requestId, `Failed to get inputs: ${errorMessage}`);
1737
+ }
1738
+ return;
1739
+ }
1740
+ const inputData = import_ethers4.ethers.toUtf8String(inputBytes);
1741
+ console.log(
1742
+ ` \u{1F4E5} Inputs received: ${inputData.substring(0, 100)}${inputData.length > 100 ? "..." : ""}`
1743
+ );
1744
+ console.log(` \u2699\uFE0F Executing...`);
1745
+ const result = await this.containerManager.runContainer(
1746
+ container,
1747
+ inputData,
1748
+ 3e5
1749
+ // 5 min timeout
1750
+ );
1751
+ if (result.exitCode !== 0) {
1752
+ console.error(` \u274C Container execution failed with exit code ${result.exitCode}`);
1753
+ console.error(` \u{1F4C4} Container output:`, result.output);
1754
+ if (this.options.onRequestFailed) {
1755
+ this.options.onRequestFailed(event.requestId, `Container execution failed with exit code ${result.exitCode}`);
1756
+ }
1757
+ return;
1758
+ }
1759
+ console.log(` \u2713 Execution completed in ${result.executionTime}ms`);
1760
+ console.log(` \u{1F4E4} Submitting result...`);
1761
+ const input = inputData;
1762
+ const output = result.output;
1763
+ const proof = event.verifier ? result.output : "";
1764
+ const subscriptionWallet = event.walletAddress;
1765
+ const commitment = CommitmentUtils.fromEvent(event, subscriptionWallet);
1766
+ const commitmentData = CommitmentUtils.encode(commitment);
1767
+ const nodeWallet = this.paymentWallet || this.walletManager.getAddress();
1768
+ const tx = await this.coordinator.reportComputeResult(
1769
+ event.interval,
1770
+ import_ethers4.ethers.toUtf8Bytes(input),
1771
+ import_ethers4.ethers.toUtf8Bytes(output),
1772
+ import_ethers4.ethers.toUtf8Bytes(proof),
1773
+ commitmentData,
1774
+ nodeWallet
1775
+ );
1776
+ console.log(` \u{1F4E4} Transaction sent: ${tx.hash}`);
1777
+ const receipt = await tx.wait();
1778
+ if (receipt.status === 1) {
1779
+ console.log(` \u2713 Result delivered successfully (block ${receipt.blockNumber})`);
1780
+ console.log(` \u{1F4B0} Fee earned: ${import_ethers4.ethers.formatEther(event.feeAmount)} ETH`);
1781
+ if (this.options.onComputeDelivered) {
1782
+ this.options.onComputeDelivered({
1783
+ requestId: event.requestId,
1784
+ subscriptionId: Number(event.subscriptionId),
1785
+ interval: Number(event.interval),
1786
+ containerId: event.containerId,
1787
+ redundancy: event.redundancy,
1788
+ feeAmount: event.feeAmount.toString(),
1789
+ feeToken: event.feeToken,
1790
+ input,
1791
+ output,
1792
+ txHash: tx.hash,
1793
+ blockNumber: receipt.blockNumber,
1794
+ gasUsed: receipt.gasUsed,
1795
+ gasPrice: receipt.gasPrice || tx.gasPrice || 0n
1796
+ });
1797
+ }
1798
+ } else {
1799
+ throw new Error(`Delivery transaction failed with status ${receipt.status}`);
1800
+ }
1801
+ } catch (error) {
1802
+ const errorMessage = error.message || String(error);
1803
+ const errorCode = error.code;
1804
+ if (errorCode === "NONCE_EXPIRED" || errorMessage.includes("nonce has already been used") || errorMessage.includes("nonce too low")) {
1805
+ console.log(` \u26A0\uFE0F Nonce expired (likely already processed by another handler)`);
1806
+ return;
1807
+ }
1808
+ console.error(` \u274C Error processing request:`, error);
1809
+ if (this.options.onRequestFailed) {
1810
+ this.options.onRequestFailed(event.requestId, errorMessage);
1811
+ }
1812
+ } finally {
1813
+ this.processingRequests.delete(event.requestId);
1814
+ }
1815
+ }
1816
+ /**
1817
+ * Self-coordination: Calculate priority and wait
1818
+ */
1819
+ async waitForPriority(event) {
1820
+ const priority = this.calculatePriority(event.requestId);
1821
+ const maxDelay = event.redundancy === 1 ? 1e3 : 200;
1822
+ const delay = Math.floor(priority / 4294967295 * maxDelay);
1823
+ if (delay > 0) {
1824
+ console.log(
1825
+ ` \u23F1\uFE0F Priority wait: ${delay}ms (priority: 0x${priority.toString(16).slice(0, 8)})`
1826
+ );
1827
+ await new Promise((resolve) => setTimeout(resolve, delay));
1828
+ }
1829
+ }
1830
+ /**
1831
+ * Calculate deterministic priority for this agent and request
1832
+ */
1833
+ calculatePriority(requestId) {
1834
+ const hash = import_ethers4.ethers.keccak256(import_ethers4.ethers.concat([requestId, this.walletManager.getAddress()]));
1835
+ return parseInt(hash.slice(2, 10), 16);
1836
+ }
1837
+ async stop() {
1838
+ console.log("Stopping Noosphere Agent...");
1839
+ await this.eventMonitor.stop();
1840
+ this.scheduler.stop();
1841
+ await this.containerManager.cleanup();
1842
+ await this.containerManager.stopPersistentContainers();
1843
+ this.isRunning = false;
1844
+ console.log("\u2713 Agent stopped");
1845
+ }
1846
+ getStatus() {
1847
+ return {
1848
+ running: this.isRunning,
1849
+ address: this.walletManager.getAddress(),
1850
+ scheduler: this.scheduler.getStats(),
1851
+ containers: {
1852
+ runningCount: this.containerManager.getRunningContainerCount()
1853
+ }
1854
+ };
1855
+ }
1856
+ /**
1857
+ * Get scheduler service (for advanced usage)
1858
+ */
1859
+ getScheduler() {
1860
+ return this.scheduler;
1861
+ }
1862
+ };
1863
+
1864
+ // src/types/index.ts
1865
+ var FulfillResult = /* @__PURE__ */ ((FulfillResult2) => {
1866
+ FulfillResult2[FulfillResult2["FULFILLED"] = 0] = "FULFILLED";
1867
+ FulfillResult2[FulfillResult2["INVALID_REQUEST_ID"] = 1] = "INVALID_REQUEST_ID";
1868
+ FulfillResult2[FulfillResult2["INVALID_COMMITMENT"] = 2] = "INVALID_COMMITMENT";
1869
+ FulfillResult2[FulfillResult2["SUBSCRIPTION_BALANCE_INVARIANT_VIOLATION"] = 3] = "SUBSCRIPTION_BALANCE_INVARIANT_VIOLATION";
1870
+ FulfillResult2[FulfillResult2["INSUFFICIENT_SUBSCRIPTION_BALANCE"] = 4] = "INSUFFICIENT_SUBSCRIPTION_BALANCE";
1871
+ FulfillResult2[FulfillResult2["COST_EXCEEDS_COMMITMENT"] = 5] = "COST_EXCEEDS_COMMITMENT";
1872
+ return FulfillResult2;
1873
+ })(FulfillResult || {});
1874
+
1875
+ // src/utils/RequestIdUtils.ts
1876
+ var import_ethers5 = require("ethers");
1877
+ var RequestIdUtils = class {
1878
+ /**
1879
+ * Pack subscriptionId and interval into requestId
1880
+ * Matches Solidity: keccak256(abi.encodePacked(subscriptionId, interval))
1881
+ */
1882
+ static pack(subscriptionId, interval) {
1883
+ const subscriptionIdBytes = import_ethers5.ethers.zeroPadValue(import_ethers5.ethers.toBeHex(subscriptionId), 8);
1884
+ const intervalBytes = import_ethers5.ethers.zeroPadValue(import_ethers5.ethers.toBeHex(interval), 4);
1885
+ const packed = import_ethers5.ethers.concat([subscriptionIdBytes, intervalBytes]);
1886
+ return import_ethers5.ethers.keccak256(packed);
1887
+ }
1888
+ /**
1889
+ * Unpack requestId into subscriptionId and interval (if stored separately)
1890
+ * Note: This is not possible from the hash alone - only for informational purposes
1891
+ * In practice, subscriptionId and interval are stored in events
1892
+ */
1893
+ static format(requestId, subscriptionId, interval) {
1894
+ return `Request(id=${requestId.slice(0, 10)}..., sub=${subscriptionId}, interval=${interval})`;
1895
+ }
1896
+ };
1897
+
1898
+ // src/index.ts
1899
+ var import_crypto2 = require("@noosphere/crypto");
1900
+ var import_registry2 = require("@noosphere/registry");
1901
+ // Annotate the CommonJS export names for ESM import in node:
1902
+ 0 && (module.exports = {
1903
+ CommitmentUtils,
1904
+ ConfigLoader,
1905
+ ContainerManager,
1906
+ EventMonitor,
1907
+ FulfillResult,
1908
+ KeystoreManager,
1909
+ NoosphereAgent,
1910
+ RegistryManager,
1911
+ RequestIdUtils,
1912
+ SchedulerService,
1913
+ WalletManager
1914
+ });
1915
+ //# sourceMappingURL=index.cjs.map