@orgloop/core 0.1.5 → 0.1.6

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 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,qBAAa,cAAc;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAE7D,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAOtC,8EAA8E;IAC9E,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;IAQpD,4BAA4B;IAC5B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;IAI7C,4CAA4C;IAC5C,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI1B,mCAAmC;IACnC,IAAI,IAAI,cAAc,EAAE;IAIxB,uCAAuC;IACvC,IAAI,IAAI,IAAI,MAAM,CAEjB;CACD"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * ModuleRegistry — singleton registry for loaded module instances.
3
+ *
4
+ * Tracks all active modules by name. Names are unique — attempting to
5
+ * register a duplicate throws ModuleConflictError.
6
+ */
7
+ import { ModuleConflictError } from './errors.js';
8
+ export class ModuleRegistry {
9
+ modules = new Map();
10
+ /** Register a module. Throws ModuleConflictError if name already exists. */
11
+ register(module) {
12
+ if (this.modules.has(module.name)) {
13
+ throw new ModuleConflictError(module.name, `Module "${module.name}" is already loaded`);
14
+ }
15
+ this.modules.set(module.name, module);
16
+ }
17
+ /** Unregister a module by name. Returns the removed instance or undefined. */
18
+ unregister(name) {
19
+ const module = this.modules.get(name);
20
+ if (module) {
21
+ this.modules.delete(name);
22
+ }
23
+ return module;
24
+ }
25
+ /** Get a module by name. */
26
+ get(name) {
27
+ return this.modules.get(name);
28
+ }
29
+ /** Check if a module name is registered. */
30
+ has(name) {
31
+ return this.modules.has(name);
32
+ }
33
+ /** List all registered modules. */
34
+ list() {
35
+ return [...this.modules.values()];
36
+ }
37
+ /** Get count of registered modules. */
38
+ get size() {
39
+ return this.modules.size;
40
+ }
41
+ }
42
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAGlD,MAAM,OAAO,cAAc;IACT,OAAO,GAAG,IAAI,GAAG,EAA0B,CAAC;IAE7D,4EAA4E;IAC5E,QAAQ,CAAC,MAAsB;QAC9B,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;YACnC,MAAM,IAAI,mBAAmB,CAAC,MAAM,CAAC,IAAI,EAAE,WAAW,MAAM,CAAC,IAAI,qBAAqB,CAAC,CAAC;QACzF,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACvC,CAAC;IAED,8EAA8E;IAC9E,UAAU,CAAC,IAAY;QACtB,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,MAAM,EAAE,CAAC;YACZ,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC3B,CAAC;QACD,OAAO,MAAM,CAAC;IACf,CAAC;IAED,4BAA4B;IAC5B,GAAG,CAAC,IAAY;QACf,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;IAED,4CAA4C;IAC5C,GAAG,CAAC,IAAY;QACf,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;IAED,mCAAmC;IACnC,IAAI;QACH,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACnC,CAAC;IAED,uCAAuC;IACvC,IAAI,IAAI;QACP,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;IAC1B,CAAC;CACD"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Runtime — the long-lived host process.
3
+ *
4
+ * Owns shared infrastructure (bus, scheduler, logger, webhook server)
5
+ * and manages modules through a registry. Modules are loaded/unloaded
6
+ * independently without affecting each other.
7
+ */
8
+ import { EventEmitter } from 'node:events';
9
+ import type { ModuleStatus, OrgLoopEvent, RuntimeStatus } from '@orgloop/sdk';
10
+ import type { EventBus } from './bus.js';
11
+ import type { RuntimeControl } from './http.js';
12
+ import { LoggerManager } from './logger.js';
13
+ import type { ModuleConfig } from './module-instance.js';
14
+ import type { CheckpointStore } from './store.js';
15
+ export interface SourceCircuitBreakerOptions {
16
+ /** Consecutive failures before opening circuit (default: 5) */
17
+ failureThreshold?: number;
18
+ /** Backoff period in ms before retry when circuit is open (default: 60000) */
19
+ retryAfterMs?: number;
20
+ }
21
+ export interface RuntimeOptions {
22
+ /** Custom event bus (default: InMemoryBus) */
23
+ bus?: EventBus;
24
+ /** Shared logger manager (default: new LoggerManager) */
25
+ loggerManager?: LoggerManager;
26
+ /** HTTP port for webhook server and control API (default: 4800) */
27
+ httpPort?: number;
28
+ /** Circuit breaker options for source polling */
29
+ circuitBreaker?: SourceCircuitBreakerOptions;
30
+ /** Data directory for checkpoints and WAL */
31
+ dataDir?: string;
32
+ }
33
+ export interface LoadModuleOptions {
34
+ /** Pre-instantiated source connectors (keyed by source ID) */
35
+ sources?: Map<string, import('@orgloop/sdk').SourceConnector>;
36
+ /** Pre-instantiated actor connectors (keyed by actor ID) */
37
+ actors?: Map<string, import('@orgloop/sdk').ActorConnector>;
38
+ /** Pre-instantiated package transforms (keyed by transform name) */
39
+ transforms?: Map<string, import('@orgloop/sdk').Transform>;
40
+ /** Pre-instantiated loggers (keyed by logger name) */
41
+ loggers?: Map<string, import('@orgloop/sdk').Logger>;
42
+ /** Custom checkpoint store for this module */
43
+ checkpointStore?: CheckpointStore;
44
+ }
45
+ declare class Runtime extends EventEmitter implements RuntimeControl {
46
+ private readonly bus;
47
+ private readonly scheduler;
48
+ private readonly loggerManager;
49
+ private readonly registry;
50
+ private readonly webhookServer;
51
+ private running;
52
+ private httpStarted;
53
+ private startedAt;
54
+ private readonly httpPort;
55
+ private readonly dataDir?;
56
+ private readonly circuitBreakerOpts;
57
+ private readonly circuitRetryTimers;
58
+ private readonly moduleConfigs;
59
+ private readonly moduleLoadOptions;
60
+ constructor(options?: RuntimeOptions);
61
+ start(): Promise<void>;
62
+ /** Start the HTTP server for webhooks and control API. */
63
+ startHttpServer(): Promise<void>;
64
+ /** Whether the HTTP server is currently running. */
65
+ isHttpStarted(): boolean;
66
+ stop(): Promise<void>;
67
+ loadModule(config: ModuleConfig, options?: LoadModuleOptions): Promise<ModuleStatus>;
68
+ unloadModule(name: string): Promise<void>;
69
+ reloadModule(name: string): Promise<void>;
70
+ inject(event: OrgLoopEvent, moduleName?: string): Promise<void>;
71
+ private processEvent;
72
+ private deliverToActor;
73
+ private pollSource;
74
+ private scheduleCircuitRetry;
75
+ status(): RuntimeStatus;
76
+ listModules(): ModuleStatus[];
77
+ getModuleStatus(name: string): ModuleStatus | undefined;
78
+ private emitLog;
79
+ }
80
+ export { Runtime };
81
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,519 @@
1
+ /**
2
+ * Runtime — the long-lived host process.
3
+ *
4
+ * Owns shared infrastructure (bus, scheduler, logger, webhook server)
5
+ * and manages modules through a registry. Modules are loaded/unloaded
6
+ * independently without affecting each other.
7
+ */
8
+ import { EventEmitter } from 'node:events';
9
+ import { readFile } from 'node:fs/promises';
10
+ import { generateTraceId } from '@orgloop/sdk';
11
+ import { InMemoryBus } from './bus.js';
12
+ import { ConnectorError, DeliveryError, ModuleNotFoundError, RuntimeError } from './errors.js';
13
+ import { DEFAULT_HTTP_PORT, WebhookServer } from './http.js';
14
+ import { LoggerManager } from './logger.js';
15
+ import { ModuleInstance } from './module-instance.js';
16
+ import { ModuleRegistry } from './registry.js';
17
+ import { matchRoutes } from './router.js';
18
+ import { Scheduler } from './scheduler.js';
19
+ import { InMemoryCheckpointStore } from './store.js';
20
+ import { executeTransformPipeline } from './transform.js';
21
+ // ─── Runtime Class ───────────────────────────────────────────────────────────
22
+ class Runtime extends EventEmitter {
23
+ // Shared infrastructure
24
+ bus;
25
+ scheduler = new Scheduler();
26
+ loggerManager;
27
+ registry = new ModuleRegistry();
28
+ webhookServer;
29
+ // Running state
30
+ running = false;
31
+ httpStarted = false;
32
+ startedAt = 0;
33
+ httpPort;
34
+ dataDir;
35
+ // Circuit breaker
36
+ circuitBreakerOpts;
37
+ circuitRetryTimers = new Map();
38
+ // Stored configs for reload
39
+ moduleConfigs = new Map();
40
+ moduleLoadOptions = new Map();
41
+ constructor(options) {
42
+ super();
43
+ this.bus = options?.bus ?? new InMemoryBus();
44
+ this.loggerManager = options?.loggerManager ?? new LoggerManager();
45
+ this.httpPort =
46
+ options?.httpPort ??
47
+ (process.env.ORGLOOP_PORT
48
+ ? Number.parseInt(process.env.ORGLOOP_PORT, 10)
49
+ : DEFAULT_HTTP_PORT);
50
+ this.dataDir = options?.dataDir;
51
+ this.circuitBreakerOpts = {
52
+ failureThreshold: options?.circuitBreaker?.failureThreshold ?? 5,
53
+ retryAfterMs: options?.circuitBreaker?.retryAfterMs ?? 60_000,
54
+ };
55
+ this.webhookServer = new WebhookServer((event) => this.inject(event));
56
+ }
57
+ // ─── Lifecycle ───────────────────────────────────────────────────────────
58
+ async start() {
59
+ if (this.running)
60
+ return;
61
+ // Start scheduler
62
+ this.scheduler.start((sourceId, moduleName) => this.pollSource(sourceId, moduleName));
63
+ this.running = true;
64
+ this.startedAt = Date.now();
65
+ await this.emitLog('runtime.start', { result: 'started' });
66
+ }
67
+ /** Start the HTTP server for webhooks and control API. */
68
+ async startHttpServer() {
69
+ if (this.httpStarted)
70
+ return;
71
+ this.webhookServer.runtime = this;
72
+ await this.webhookServer.start(this.httpPort);
73
+ this.httpStarted = true;
74
+ }
75
+ /** Whether the HTTP server is currently running. */
76
+ isHttpStarted() {
77
+ return this.httpStarted;
78
+ }
79
+ async stop() {
80
+ if (!this.running)
81
+ return;
82
+ await this.emitLog('runtime.stop', { result: 'stopping' });
83
+ // Deactivate and shutdown all modules
84
+ for (const mod of this.registry.list()) {
85
+ try {
86
+ mod.deactivate();
87
+ this.scheduler.removeSources(mod.name);
88
+ await mod.shutdown();
89
+ }
90
+ catch {
91
+ // Non-blocking
92
+ }
93
+ }
94
+ // Stop webhook server
95
+ if (this.httpStarted) {
96
+ await this.webhookServer.stop();
97
+ this.httpStarted = false;
98
+ }
99
+ // Stop scheduler
100
+ this.scheduler.stop();
101
+ // Clear circuit breaker timers
102
+ for (const timer of this.circuitRetryTimers.values()) {
103
+ clearTimeout(timer);
104
+ }
105
+ this.circuitRetryTimers.clear();
106
+ // Flush and shutdown loggers
107
+ await this.loggerManager.flush();
108
+ await this.loggerManager.shutdown();
109
+ this.running = false;
110
+ }
111
+ // ─── Module Management ───────────────────────────────────────────────────
112
+ async loadModule(config, options) {
113
+ const checkpointStore = options?.checkpointStore ?? new InMemoryCheckpointStore();
114
+ const mod = new ModuleInstance(config, {
115
+ sources: options?.sources ?? new Map(),
116
+ actors: options?.actors ?? new Map(),
117
+ transforms: options?.transforms ?? new Map(),
118
+ loggers: options?.loggers ?? new Map(),
119
+ checkpointStore,
120
+ });
121
+ // Initialize all connectors
122
+ await mod.initialize();
123
+ // Activate and register before adding to scheduler
124
+ // (so the first poll finds the module in the registry)
125
+ mod.activate();
126
+ this.registry.register(mod);
127
+ // Add module loggers to shared LoggerManager (tagged with module name)
128
+ for (const [, logger] of mod.getLoggers()) {
129
+ this.loggerManager.addLogger(logger, mod.name);
130
+ }
131
+ // Register poll sources with shared scheduler
132
+ const defaultInterval = config.defaults?.poll_interval ?? '5m';
133
+ let hasWebhooks = false;
134
+ for (const srcCfg of config.sources) {
135
+ const connector = mod.getSource(srcCfg.id);
136
+ if (!connector)
137
+ continue;
138
+ if (typeof connector.webhook === 'function') {
139
+ // Webhook-based source: register with shared server
140
+ this.webhookServer.addHandler(srcCfg.id, connector.webhook());
141
+ hasWebhooks = true;
142
+ }
143
+ else {
144
+ // Poll-based source: register with shared scheduler
145
+ const interval = srcCfg.poll?.interval ?? defaultInterval;
146
+ this.scheduler.addSource(srcCfg.id, interval, mod.name);
147
+ }
148
+ }
149
+ // Start HTTP server on demand when webhook sources are present
150
+ if (hasWebhooks && !this.httpStarted) {
151
+ await this.startHttpServer();
152
+ }
153
+ // Store config and options for reload
154
+ this.moduleConfigs.set(config.name, config);
155
+ if (options) {
156
+ this.moduleLoadOptions.set(config.name, options);
157
+ }
158
+ await this.emitLog('module.active', {
159
+ result: `module "${config.name}" loaded`,
160
+ module: config.name,
161
+ });
162
+ return mod.status();
163
+ }
164
+ async unloadModule(name) {
165
+ const mod = this.registry.get(name);
166
+ if (!mod) {
167
+ throw new ModuleNotFoundError(name);
168
+ }
169
+ await this.emitLog('module.unloading', {
170
+ result: `unloading module "${name}"`,
171
+ module: name,
172
+ });
173
+ // Deactivate
174
+ mod.deactivate();
175
+ // Remove sources from scheduler
176
+ this.scheduler.removeSources(name);
177
+ // Remove webhook handlers for module sources
178
+ for (const srcCfg of mod.config.sources) {
179
+ this.webhookServer.removeHandler(srcCfg.id);
180
+ }
181
+ // Shutdown module resources
182
+ await mod.shutdown();
183
+ // Remove loggers by module tag
184
+ this.loggerManager.removeLoggersByTag(name);
185
+ // Unregister from registry
186
+ this.registry.unregister(name);
187
+ // Clean stored config
188
+ this.moduleConfigs.delete(name);
189
+ this.moduleLoadOptions.delete(name);
190
+ await this.emitLog('module.removed', {
191
+ result: `module "${name}" removed`,
192
+ module: name,
193
+ });
194
+ }
195
+ async reloadModule(name) {
196
+ const config = this.moduleConfigs.get(name);
197
+ if (!config) {
198
+ throw new ModuleNotFoundError(name);
199
+ }
200
+ const options = this.moduleLoadOptions.get(name);
201
+ await this.unloadModule(name);
202
+ await this.loadModule(config, options);
203
+ }
204
+ // ─── Event Processing ────────────────────────────────────────────────────
205
+ async inject(event, moduleName) {
206
+ const resolved = event.trace_id ? event : { ...event, trace_id: generateTraceId() };
207
+ if (moduleName) {
208
+ const mod = this.registry.get(moduleName);
209
+ if (!mod) {
210
+ throw new ModuleNotFoundError(moduleName);
211
+ }
212
+ await this.processEvent(resolved, mod);
213
+ }
214
+ else {
215
+ // Process through all active modules
216
+ for (const mod of this.registry.list()) {
217
+ if (mod.getState() === 'active') {
218
+ await this.processEvent(resolved, mod);
219
+ }
220
+ }
221
+ }
222
+ }
223
+ async processEvent(event, mod) {
224
+ this.emit('event', event);
225
+ await this.emitLog('source.emit', {
226
+ event_id: event.id,
227
+ trace_id: event.trace_id,
228
+ source: event.source,
229
+ event_type: event.type,
230
+ module: mod.name,
231
+ });
232
+ // Write to bus (WAL)
233
+ await this.bus.publish(event);
234
+ // Match routes from this module
235
+ const matched = matchRoutes(event, mod.getRoutes());
236
+ if (matched.length === 0) {
237
+ await this.emitLog('route.no_match', {
238
+ event_id: event.id,
239
+ trace_id: event.trace_id,
240
+ source: event.source,
241
+ module: mod.name,
242
+ });
243
+ await this.bus.ack(event.id);
244
+ return;
245
+ }
246
+ // Process each matched route
247
+ for (const match of matched) {
248
+ const { route } = match;
249
+ await this.emitLog('route.match', {
250
+ event_id: event.id,
251
+ trace_id: event.trace_id,
252
+ route: route.name,
253
+ source: event.source,
254
+ target: route.then.actor,
255
+ module: mod.name,
256
+ });
257
+ // Run transform pipeline
258
+ let transformedEvent = event;
259
+ if (route.transforms && route.transforms.length > 0) {
260
+ const pipelineOptions = {
261
+ definitions: mod.config.transforms,
262
+ packageTransforms: mod.getTransformsMap(),
263
+ onLog: (partial) => {
264
+ void this.emitLog(partial.phase ?? 'transform.start', {
265
+ ...partial,
266
+ event_id: partial.event_id ?? event.id,
267
+ trace_id: partial.trace_id ?? event.trace_id,
268
+ route: route.name,
269
+ module: mod.name,
270
+ });
271
+ },
272
+ };
273
+ const context = {
274
+ source: event.source,
275
+ target: route.then.actor,
276
+ eventType: event.type,
277
+ routeName: route.name,
278
+ };
279
+ try {
280
+ const result = await executeTransformPipeline(event, context, route.transforms, pipelineOptions);
281
+ if (result.dropped || !result.event) {
282
+ continue; // Skip delivery for this route
283
+ }
284
+ transformedEvent = result.event;
285
+ }
286
+ catch (err) {
287
+ // halt policy throws TransformError — emit error and skip delivery
288
+ this.emit('error', err);
289
+ continue;
290
+ }
291
+ }
292
+ // Deliver to actor
293
+ await this.deliverToActor(transformedEvent, route.name, route.then.actor, route, mod);
294
+ }
295
+ // Ack the event after all routes processed
296
+ await this.bus.ack(event.id);
297
+ }
298
+ async deliverToActor(event, routeName, actorId, route, mod) {
299
+ const actor = mod.getActor(actorId);
300
+ if (!actor) {
301
+ const error = new DeliveryError(actorId, routeName, `Actor "${actorId}" not found`);
302
+ this.emit('error', error);
303
+ return;
304
+ }
305
+ await this.emitLog('deliver.attempt', {
306
+ event_id: event.id,
307
+ trace_id: event.trace_id,
308
+ route: routeName,
309
+ target: actorId,
310
+ module: mod.name,
311
+ });
312
+ const startTime = Date.now();
313
+ try {
314
+ // Build delivery config
315
+ const deliveryConfig = {
316
+ ...(route.then.config ?? {}),
317
+ };
318
+ // Resolve launch prompt if configured
319
+ if (route.with?.prompt_file) {
320
+ try {
321
+ const promptContent = await readFile(route.with.prompt_file, 'utf-8');
322
+ deliveryConfig.launch_prompt = promptContent;
323
+ deliveryConfig.launch_prompt_file = route.with.prompt_file;
324
+ }
325
+ catch {
326
+ // Non-fatal: log but continue delivery
327
+ }
328
+ }
329
+ const result = await actor.deliver(event, deliveryConfig);
330
+ const durationMs = Date.now() - startTime;
331
+ if (result.status === 'delivered') {
332
+ await this.emitLog('deliver.success', {
333
+ event_id: event.id,
334
+ trace_id: event.trace_id,
335
+ route: routeName,
336
+ target: actorId,
337
+ duration_ms: durationMs,
338
+ module: mod.name,
339
+ });
340
+ this.emit('delivery', {
341
+ event,
342
+ route: routeName,
343
+ actor: actorId,
344
+ status: 'delivered',
345
+ });
346
+ }
347
+ else {
348
+ await this.emitLog('deliver.failure', {
349
+ event_id: event.id,
350
+ trace_id: event.trace_id,
351
+ route: routeName,
352
+ target: actorId,
353
+ duration_ms: durationMs,
354
+ error: result.error?.message ?? result.status,
355
+ module: mod.name,
356
+ });
357
+ this.emit('delivery', {
358
+ event,
359
+ route: routeName,
360
+ actor: actorId,
361
+ status: result.status,
362
+ });
363
+ }
364
+ }
365
+ catch (err) {
366
+ const durationMs = Date.now() - startTime;
367
+ const error = new DeliveryError(actorId, routeName, 'Delivery failed', { cause: err });
368
+ this.emit('error', error);
369
+ await this.emitLog('deliver.failure', {
370
+ event_id: event.id,
371
+ trace_id: event.trace_id,
372
+ route: routeName,
373
+ target: actorId,
374
+ duration_ms: durationMs,
375
+ error: error.message,
376
+ module: mod.name,
377
+ });
378
+ }
379
+ }
380
+ // ─── Source Polling ───────────────────────────────────────────────────────
381
+ async pollSource(sourceId, moduleName) {
382
+ if (!moduleName)
383
+ return;
384
+ const mod = this.registry.get(moduleName);
385
+ if (!mod || mod.getState() !== 'active')
386
+ return;
387
+ const connector = mod.getSource(sourceId);
388
+ if (!connector)
389
+ return;
390
+ const healthState = mod.getHealthState(sourceId);
391
+ if (!healthState)
392
+ return;
393
+ // Circuit breaker: skip poll if circuit is open
394
+ if (healthState.circuitOpen) {
395
+ return;
396
+ }
397
+ healthState.lastPollAttempt = new Date().toISOString();
398
+ try {
399
+ const store = mod.getCheckpointStore();
400
+ const checkpoint = await store.get(sourceId);
401
+ const result = await connector.poll(checkpoint);
402
+ // Save checkpoint
403
+ if (result.checkpoint) {
404
+ await store.set(sourceId, result.checkpoint);
405
+ }
406
+ // Record successful poll
407
+ healthState.lastSuccessfulPoll = new Date().toISOString();
408
+ healthState.lastError = null;
409
+ healthState.totalEventsEmitted += result.events.length;
410
+ // If recovering from errors, log recovery
411
+ if (healthState.consecutiveErrors > 0) {
412
+ await this.emitLog('source.circuit_close', {
413
+ source: sourceId,
414
+ result: `recovered after ${healthState.consecutiveErrors} consecutive errors`,
415
+ module: moduleName,
416
+ });
417
+ }
418
+ healthState.consecutiveErrors = 0;
419
+ healthState.status = 'healthy';
420
+ // Process each event through the module
421
+ for (const event of result.events) {
422
+ const enriched = event.trace_id ? event : { ...event, trace_id: generateTraceId() };
423
+ await this.processEvent(enriched, mod);
424
+ }
425
+ }
426
+ catch (err) {
427
+ const error = new ConnectorError(sourceId, 'Poll failed', { cause: err });
428
+ this.emit('error', error);
429
+ healthState.consecutiveErrors++;
430
+ healthState.lastError = err instanceof Error ? err.message : String(err);
431
+ // Update health status
432
+ if (healthState.consecutiveErrors >= this.circuitBreakerOpts.failureThreshold) {
433
+ healthState.status = 'unhealthy';
434
+ healthState.circuitOpen = true;
435
+ await this.emitLog('source.circuit_open', {
436
+ source: sourceId,
437
+ error: healthState.lastError,
438
+ result: `${healthState.consecutiveErrors} consecutive failures — polling paused, will retry in ${Math.round(this.circuitBreakerOpts.retryAfterMs / 1000)}s`,
439
+ module: moduleName,
440
+ });
441
+ // Schedule a retry after backoff
442
+ this.scheduleCircuitRetry(sourceId, moduleName);
443
+ }
444
+ else {
445
+ healthState.status = 'degraded';
446
+ await this.emitLog('system.error', {
447
+ source: sourceId,
448
+ error: error.message,
449
+ module: moduleName,
450
+ });
451
+ }
452
+ }
453
+ }
454
+ scheduleCircuitRetry(sourceId, moduleName) {
455
+ const timerKey = `${moduleName}/${sourceId}`;
456
+ const existing = this.circuitRetryTimers.get(timerKey);
457
+ if (existing)
458
+ clearTimeout(existing);
459
+ const timer = setTimeout(async () => {
460
+ this.circuitRetryTimers.delete(timerKey);
461
+ if (!this.running)
462
+ return;
463
+ const mod = this.registry.get(moduleName);
464
+ if (!mod || mod.getState() !== 'active')
465
+ return;
466
+ const healthState = mod.getHealthState(sourceId);
467
+ if (!healthState || !healthState.circuitOpen)
468
+ return;
469
+ await this.emitLog('source.circuit_retry', {
470
+ source: sourceId,
471
+ result: 'attempting recovery poll',
472
+ module: moduleName,
473
+ });
474
+ // Temporarily allow poll by opening the circuit
475
+ healthState.circuitOpen = false;
476
+ await this.pollSource(sourceId, moduleName);
477
+ }, this.circuitBreakerOpts.retryAfterMs);
478
+ this.circuitRetryTimers.set(timerKey, timer);
479
+ }
480
+ // ─── RuntimeControl Implementation ───────────────────────────────────────
481
+ status() {
482
+ return {
483
+ running: this.running,
484
+ pid: process.pid,
485
+ uptime_ms: this.running ? Date.now() - this.startedAt : 0,
486
+ httpPort: this.httpPort,
487
+ modules: this.registry.list().map((m) => m.status()),
488
+ };
489
+ }
490
+ listModules() {
491
+ return this.registry.list().map((m) => m.status());
492
+ }
493
+ getModuleStatus(name) {
494
+ const mod = this.registry.get(name);
495
+ return mod?.status();
496
+ }
497
+ // ─── Logging ─────────────────────────────────────────────────────────────
498
+ async emitLog(phase, fields) {
499
+ const entry = {
500
+ timestamp: new Date().toISOString(),
501
+ event_id: fields.event_id ?? '',
502
+ trace_id: fields.trace_id ?? '',
503
+ phase,
504
+ source: fields.source,
505
+ target: fields.target,
506
+ route: fields.route,
507
+ transform: fields.transform,
508
+ event_type: fields.event_type,
509
+ result: fields.result,
510
+ duration_ms: fields.duration_ms,
511
+ error: fields.error,
512
+ metadata: fields.metadata,
513
+ module: fields.module,
514
+ };
515
+ await this.loggerManager.log(entry);
516
+ }
517
+ }
518
+ export { Runtime };
519
+ //# sourceMappingURL=runtime.js.map