pnpfucius 2.0.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.
@@ -0,0 +1,168 @@
1
+ // Graceful shutdown and signal handling for daemon mode
2
+ // Ensures state is saved and resources are cleaned up
3
+
4
+ import { agentEvents, AgentEvents } from '../events/emitter.js';
5
+
6
+ export function setupGracefulShutdown(daemon, options = {}) {
7
+ const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
8
+ let shuttingDown = false;
9
+ const timeout = options.timeout || 30000;
10
+
11
+ const shutdown = async (signal) => {
12
+ if (shuttingDown) {
13
+ console.log('Shutdown already in progress...');
14
+ return;
15
+ }
16
+
17
+ shuttingDown = true;
18
+ console.log(`\nReceived ${signal}, shutting down gracefully...`);
19
+
20
+ // Set a hard timeout
21
+ const hardTimeout = setTimeout(() => {
22
+ console.error('Shutdown timeout exceeded, forcing exit');
23
+ process.exit(1);
24
+ }, timeout);
25
+
26
+ try {
27
+ agentEvents.emitTyped(AgentEvents.DAEMON_STOPPED, {
28
+ signal,
29
+ reason: 'graceful_shutdown'
30
+ });
31
+
32
+ await daemon.stop();
33
+ console.log('Daemon stopped successfully');
34
+
35
+ clearTimeout(hardTimeout);
36
+ process.exit(0);
37
+ } catch (error) {
38
+ console.error('Error during shutdown:', error.message);
39
+ clearTimeout(hardTimeout);
40
+ process.exit(1);
41
+ }
42
+ };
43
+
44
+ // Register signal handlers
45
+ for (const signal of signals) {
46
+ process.on(signal, () => shutdown(signal));
47
+ }
48
+
49
+ // Handle uncaught exceptions
50
+ process.on('uncaughtException', async (error) => {
51
+ console.error('Uncaught exception:', error);
52
+
53
+ agentEvents.emitTyped(AgentEvents.DAEMON_ERROR, {
54
+ type: 'uncaughtException',
55
+ error: error.message,
56
+ stack: error.stack
57
+ });
58
+
59
+ try {
60
+ if (daemon.saveState) {
61
+ await daemon.saveState();
62
+ }
63
+ } catch (saveError) {
64
+ console.error('Failed to save state:', saveError.message);
65
+ }
66
+
67
+ process.exit(1);
68
+ });
69
+
70
+ // Handle unhandled promise rejections
71
+ process.on('unhandledRejection', async (reason, promise) => {
72
+ console.error('Unhandled rejection at:', promise, 'reason:', reason);
73
+
74
+ agentEvents.emitTyped(AgentEvents.DAEMON_ERROR, {
75
+ type: 'unhandledRejection',
76
+ error: String(reason)
77
+ });
78
+
79
+ try {
80
+ if (daemon.saveState) {
81
+ await daemon.saveState();
82
+ }
83
+ } catch (saveError) {
84
+ console.error('Failed to save state:', saveError.message);
85
+ }
86
+
87
+ process.exit(1);
88
+ });
89
+
90
+ return {
91
+ shutdown,
92
+ isShuttingDown: () => shuttingDown
93
+ };
94
+ }
95
+
96
+ // Health check helper for long-running processes
97
+ export class HealthMonitor {
98
+ constructor(options = {}) {
99
+ this.checkInterval = options.checkInterval || 60000;
100
+ this.maxMemoryMB = options.maxMemoryMB || 512;
101
+ this.onUnhealthy = options.onUnhealthy || null;
102
+ this.timer = null;
103
+ this.startTime = Date.now();
104
+ this.lastCheck = null;
105
+ }
106
+
107
+ start() {
108
+ this.timer = setInterval(() => this.check(), this.checkInterval);
109
+ return this;
110
+ }
111
+
112
+ stop() {
113
+ if (this.timer) {
114
+ clearInterval(this.timer);
115
+ this.timer = null;
116
+ }
117
+ return this;
118
+ }
119
+
120
+ check() {
121
+ const health = this.getHealth();
122
+ this.lastCheck = Date.now();
123
+
124
+ if (!health.healthy && this.onUnhealthy) {
125
+ this.onUnhealthy(health);
126
+ }
127
+
128
+ return health;
129
+ }
130
+
131
+ getHealth() {
132
+ const memUsage = process.memoryUsage();
133
+ const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024);
134
+ const rssMB = Math.round(memUsage.rss / 1024 / 1024);
135
+
136
+ const uptime = Date.now() - this.startTime;
137
+ const memoryOk = heapUsedMB < this.maxMemoryMB;
138
+
139
+ return {
140
+ healthy: memoryOk,
141
+ uptime,
142
+ uptimeHuman: this.formatUptime(uptime),
143
+ memory: {
144
+ heapUsedMB,
145
+ rssMB,
146
+ maxMB: this.maxMemoryMB
147
+ },
148
+ lastCheck: this.lastCheck,
149
+ issues: memoryOk ? [] : ['Memory usage exceeded threshold']
150
+ };
151
+ }
152
+
153
+ formatUptime(ms) {
154
+ const seconds = Math.floor(ms / 1000);
155
+ const minutes = Math.floor(seconds / 60);
156
+ const hours = Math.floor(minutes / 60);
157
+ const days = Math.floor(hours / 24);
158
+
159
+ if (days > 0) return `${days}d ${hours % 24}h`;
160
+ if (hours > 0) return `${hours}h ${minutes % 60}m`;
161
+ if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
162
+ return `${seconds}s`;
163
+ }
164
+ }
165
+
166
+ export function createHealthMonitor(options) {
167
+ return new HealthMonitor(options);
168
+ }
@@ -0,0 +1,252 @@
1
+ // Cron-style scheduling engine for autonomous agent operation
2
+ // Supports both cron expressions and interval strings
3
+
4
+ import cronParser from 'cron-parser';
5
+
6
+ export class Scheduler {
7
+ constructor() {
8
+ this.tasks = new Map();
9
+ this.timers = new Map();
10
+ this.running = false;
11
+ this.taskRuns = new Map(); // Track execution counts
12
+ }
13
+
14
+ addTask(config) {
15
+ const { name, schedule, task, runImmediately = false, enabled = true } = config;
16
+
17
+ if (!name || !schedule || !task) {
18
+ throw new Error('Task requires name, schedule, and task function');
19
+ }
20
+
21
+ const parsed = this.parseSchedule(schedule);
22
+
23
+ this.tasks.set(name, {
24
+ schedule: parsed,
25
+ originalSchedule: schedule,
26
+ task,
27
+ runImmediately,
28
+ enabled,
29
+ lastRun: null,
30
+ nextRun: null,
31
+ runCount: 0,
32
+ errors: []
33
+ });
34
+
35
+ return this;
36
+ }
37
+
38
+ removeTask(name) {
39
+ const timer = this.timers.get(name);
40
+ if (timer) {
41
+ clearTimeout(timer);
42
+ this.timers.delete(name);
43
+ }
44
+ this.tasks.delete(name);
45
+ return this;
46
+ }
47
+
48
+ enableTask(name) {
49
+ const task = this.tasks.get(name);
50
+ if (task) {
51
+ task.enabled = true;
52
+ if (this.running) {
53
+ this.scheduleNext(name, task);
54
+ }
55
+ }
56
+ return this;
57
+ }
58
+
59
+ disableTask(name) {
60
+ const task = this.tasks.get(name);
61
+ if (task) {
62
+ task.enabled = false;
63
+ const timer = this.timers.get(name);
64
+ if (timer) {
65
+ clearTimeout(timer);
66
+ this.timers.delete(name);
67
+ }
68
+ }
69
+ return this;
70
+ }
71
+
72
+ parseSchedule(schedule) {
73
+ // Support cron expressions (5 or 6 fields)
74
+ if (schedule.includes('*') || schedule.split(' ').length >= 5) {
75
+ return { type: 'cron', expression: schedule };
76
+ }
77
+
78
+ // Support interval strings like "30s", "5m", "1h", "2d"
79
+ const match = schedule.match(/^(\d+)(s|m|h|d)$/i);
80
+ if (match) {
81
+ const multipliers = {
82
+ s: 1000,
83
+ m: 60 * 1000,
84
+ h: 60 * 60 * 1000,
85
+ d: 24 * 60 * 60 * 1000
86
+ };
87
+ const value = parseInt(match[1], 10);
88
+ const unit = match[2].toLowerCase();
89
+ return {
90
+ type: 'interval',
91
+ ms: value * multipliers[unit],
92
+ original: schedule
93
+ };
94
+ }
95
+
96
+ // Support plain milliseconds
97
+ const ms = parseInt(schedule, 10);
98
+ if (!isNaN(ms)) {
99
+ return { type: 'interval', ms };
100
+ }
101
+
102
+ throw new Error(`Invalid schedule format: ${schedule}. Use cron expression or interval (e.g., "5m", "1h")`);
103
+ }
104
+
105
+ start() {
106
+ if (this.running) return this;
107
+
108
+ this.running = true;
109
+
110
+ for (const [name, config] of this.tasks) {
111
+ if (!config.enabled) continue;
112
+
113
+ if (config.runImmediately) {
114
+ this.executeTask(name, config);
115
+ }
116
+ this.scheduleNext(name, config);
117
+ }
118
+
119
+ return this;
120
+ }
121
+
122
+ stop() {
123
+ this.running = false;
124
+
125
+ for (const timer of this.timers.values()) {
126
+ clearTimeout(timer);
127
+ }
128
+ this.timers.clear();
129
+
130
+ return this;
131
+ }
132
+
133
+ scheduleNext(name, config) {
134
+ if (!this.running || !config.enabled) return;
135
+
136
+ let delay;
137
+
138
+ if (config.schedule.type === 'interval') {
139
+ delay = config.schedule.ms;
140
+ } else {
141
+ try {
142
+ const interval = cronParser.parseExpression(config.schedule.expression);
143
+ const nextDate = interval.next().toDate();
144
+ delay = nextDate.getTime() - Date.now();
145
+
146
+ // Ensure minimum delay of 1 second
147
+ if (delay < 1000) {
148
+ delay = 1000;
149
+ }
150
+ } catch (error) {
151
+ console.error(`Invalid cron expression for task ${name}:`, error.message);
152
+ return;
153
+ }
154
+ }
155
+
156
+ config.nextRun = Date.now() + delay;
157
+
158
+ const timer = setTimeout(async () => {
159
+ if (!this.running || !config.enabled) return;
160
+
161
+ await this.executeTask(name, config);
162
+ this.scheduleNext(name, config);
163
+ }, delay);
164
+
165
+ this.timers.set(name, timer);
166
+ }
167
+
168
+ async executeTask(name, config) {
169
+ config.lastRun = Date.now();
170
+ config.runCount++;
171
+
172
+ try {
173
+ await config.task();
174
+ } catch (error) {
175
+ config.errors.push({
176
+ time: Date.now(),
177
+ message: error.message
178
+ });
179
+
180
+ // Keep only last 10 errors
181
+ if (config.errors.length > 10) {
182
+ config.errors.shift();
183
+ }
184
+
185
+ console.error(`Task "${name}" failed:`, error.message);
186
+ }
187
+ }
188
+
189
+ // Trigger immediate execution of a task
190
+ async runNow(name) {
191
+ const config = this.tasks.get(name);
192
+ if (!config) {
193
+ throw new Error(`Task not found: ${name}`);
194
+ }
195
+
196
+ await this.executeTask(name, config);
197
+ return this;
198
+ }
199
+
200
+ getNextRun(taskName) {
201
+ const config = this.tasks.get(taskName);
202
+ if (!config) return null;
203
+
204
+ return config.nextRun;
205
+ }
206
+
207
+ getTaskInfo(taskName) {
208
+ const config = this.tasks.get(taskName);
209
+ if (!config) return null;
210
+
211
+ return {
212
+ name: taskName,
213
+ schedule: config.originalSchedule,
214
+ enabled: config.enabled,
215
+ lastRun: config.lastRun,
216
+ nextRun: config.nextRun,
217
+ runCount: config.runCount,
218
+ recentErrors: config.errors.slice(-5)
219
+ };
220
+ }
221
+
222
+ getAllTasks() {
223
+ const tasks = [];
224
+ for (const [name, config] of this.tasks) {
225
+ tasks.push(this.getTaskInfo(name));
226
+ }
227
+ return tasks;
228
+ }
229
+
230
+ isRunning() {
231
+ return this.running;
232
+ }
233
+ }
234
+
235
+ export function createScheduler() {
236
+ return new Scheduler();
237
+ }
238
+
239
+ // Helper to format next run time
240
+ export function formatNextRun(timestamp) {
241
+ if (!timestamp) return 'Not scheduled';
242
+
243
+ const now = Date.now();
244
+ const diff = timestamp - now;
245
+
246
+ if (diff < 0) return 'Overdue';
247
+ if (diff < 60000) return `${Math.round(diff / 1000)}s`;
248
+ if (diff < 3600000) return `${Math.round(diff / 60000)}m`;
249
+ if (diff < 86400000) return `${Math.round(diff / 3600000)}h`;
250
+
251
+ return new Date(timestamp).toLocaleString();
252
+ }
@@ -0,0 +1,147 @@
1
+ // Central event bus for agent status updates and coordination
2
+ // Uses Node.js EventEmitter with typed events
3
+
4
+ import { EventEmitter } from 'events';
5
+
6
+ // Event type constants
7
+ export const AgentEvents = {
8
+ // Daemon lifecycle
9
+ DAEMON_STARTED: 'daemon:started',
10
+ DAEMON_STOPPED: 'daemon:stopped',
11
+ DAEMON_ERROR: 'daemon:error',
12
+
13
+ // Cycle events
14
+ CYCLE_START: 'cycle:start',
15
+ CYCLE_COMPLETE: 'cycle:complete',
16
+ CYCLE_ERROR: 'cycle:error',
17
+
18
+ // Market events
19
+ MARKET_CREATED: 'market:created',
20
+ MARKET_FAILED: 'market:failed',
21
+ MARKET_RESOLVED: 'market:resolved',
22
+ MARKET_UPDATED: 'market:updated',
23
+
24
+ // Transaction events
25
+ TX_SENT: 'tx:sent',
26
+ TX_CONFIRMED: 'tx:confirmed',
27
+ TX_FAILED: 'tx:failed',
28
+
29
+ // News events
30
+ NEWS_EVENT: 'news:event',
31
+ NEWS_CHECK_COMPLETE: 'news:check:complete',
32
+
33
+ // Webhook events
34
+ WEBHOOK_RECEIVED: 'webhook:received',
35
+ WEBHOOK_SERVER_STARTED: 'webhook:server:started',
36
+ WEBHOOK_SERVER_STOPPED: 'webhook:server:stopped',
37
+ HELIUS_EVENT: 'helius:event',
38
+
39
+ // State events
40
+ STATE_SAVED: 'state:saved',
41
+ STATE_RESTORED: 'state:restored',
42
+
43
+ // Analytics events
44
+ STATS_UPDATED: 'stats:updated'
45
+ };
46
+
47
+ class AgentEventEmitter extends EventEmitter {
48
+ constructor() {
49
+ super();
50
+ this.setMaxListeners(50);
51
+ this.eventHistory = [];
52
+ this.maxHistorySize = 100;
53
+ }
54
+
55
+ // Emit with timestamp and optional metadata
56
+ emitTyped(event, payload = {}) {
57
+ const eventData = {
58
+ type: event,
59
+ timestamp: Date.now(),
60
+ ...payload
61
+ };
62
+
63
+ // Store in history
64
+ this.eventHistory.unshift(eventData);
65
+ if (this.eventHistory.length > this.maxHistorySize) {
66
+ this.eventHistory.pop();
67
+ }
68
+
69
+ this.emit(event, eventData);
70
+ return this;
71
+ }
72
+
73
+ // Get recent events of a specific type
74
+ getRecentEvents(type = null, limit = 10) {
75
+ let events = this.eventHistory;
76
+
77
+ if (type) {
78
+ events = events.filter(e => e.type === type);
79
+ }
80
+
81
+ return events.slice(0, limit);
82
+ }
83
+
84
+ // Clear event history
85
+ clearHistory() {
86
+ this.eventHistory = [];
87
+ }
88
+
89
+ // Subscribe to multiple events
90
+ onMany(events, handler) {
91
+ for (const event of events) {
92
+ this.on(event, handler);
93
+ }
94
+ return this;
95
+ }
96
+
97
+ // One-time subscription to multiple events
98
+ onceAny(events, handler) {
99
+ const wrappedHandler = (data) => {
100
+ // Remove all listeners after first event
101
+ for (const event of events) {
102
+ this.off(event, wrappedHandler);
103
+ }
104
+ handler(data);
105
+ };
106
+
107
+ for (const event of events) {
108
+ this.on(event, wrappedHandler);
109
+ }
110
+ return this;
111
+ }
112
+
113
+ // Wait for a specific event with timeout
114
+ waitFor(event, timeout = 30000) {
115
+ return new Promise((resolve, reject) => {
116
+ const timer = setTimeout(() => {
117
+ this.off(event, handler);
118
+ reject(new Error(`Timeout waiting for event: ${event}`));
119
+ }, timeout);
120
+
121
+ const handler = (data) => {
122
+ clearTimeout(timer);
123
+ resolve(data);
124
+ };
125
+
126
+ this.once(event, handler);
127
+ });
128
+ }
129
+ }
130
+
131
+ // Singleton instance
132
+ export const agentEvents = new AgentEventEmitter();
133
+
134
+ // Helper to create scoped event emitters
135
+ export function createScopedEmitter(scope) {
136
+ return {
137
+ emit: (event, payload) => {
138
+ agentEvents.emitTyped(`${scope}:${event}`, payload);
139
+ },
140
+ on: (event, handler) => {
141
+ agentEvents.on(`${scope}:${event}`, handler);
142
+ },
143
+ off: (event, handler) => {
144
+ agentEvents.off(`${scope}:${event}`, handler);
145
+ }
146
+ };
147
+ }