@covibes/zeroshot 5.3.0 → 5.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +8 -385
  3. package/cli/index.js +108 -4615
  4. package/package.json +16 -125
  5. package/CHANGELOG.md +0 -537
  6. package/cli/commands/providers.js +0 -150
  7. package/cli/lib/first-run.js +0 -211
  8. package/cli/lib/update-checker.js +0 -281
  9. package/cli/message-formatter-utils.js +0 -75
  10. package/cli/message-formatters-normal.js +0 -253
  11. package/cli/message-formatters-watch.js +0 -181
  12. package/cluster-templates/base-templates/debug-workflow.json +0 -375
  13. package/cluster-templates/base-templates/full-workflow.json +0 -697
  14. package/cluster-templates/base-templates/single-worker.json +0 -63
  15. package/cluster-templates/base-templates/worker-validator.json +0 -152
  16. package/cluster-templates/conductor-bootstrap.json +0 -124
  17. package/docker/zeroshot-cluster/Dockerfile +0 -152
  18. package/docker/zeroshot-cluster/pre-baked-deps.json +0 -28
  19. package/lib/completion.js +0 -174
  20. package/lib/docker-config.js +0 -212
  21. package/lib/id-detector.js +0 -53
  22. package/lib/provider-detection.js +0 -59
  23. package/lib/provider-names.js +0 -56
  24. package/lib/settings.js +0 -405
  25. package/lib/stream-json-parser.js +0 -10
  26. package/scripts/fix-node-pty-permissions.js +0 -75
  27. package/scripts/record-demo.sh +0 -279
  28. package/scripts/test-install.sh +0 -40
  29. package/scripts/validate-templates.js +0 -100
  30. package/src/agent/agent-config.js +0 -193
  31. package/src/agent/agent-context-builder.js +0 -335
  32. package/src/agent/agent-hook-executor.js +0 -402
  33. package/src/agent/agent-lifecycle.js +0 -683
  34. package/src/agent/agent-stuck-detector.js +0 -256
  35. package/src/agent/agent-task-executor.js +0 -1431
  36. package/src/agent/agent-trigger-evaluator.js +0 -67
  37. package/src/agent/output-extraction.js +0 -200
  38. package/src/agent/output-reformatter.js +0 -175
  39. package/src/agent/schema-utils.js +0 -111
  40. package/src/agent-wrapper.js +0 -570
  41. package/src/agents/git-pusher-agent.json +0 -20
  42. package/src/attach/attach-client.js +0 -438
  43. package/src/attach/attach-server.js +0 -543
  44. package/src/attach/index.js +0 -35
  45. package/src/attach/protocol.js +0 -220
  46. package/src/attach/ring-buffer.js +0 -121
  47. package/src/attach/socket-discovery.js +0 -267
  48. package/src/claude-task-runner.js +0 -523
  49. package/src/config-router.js +0 -80
  50. package/src/config-validator.js +0 -1660
  51. package/src/copy-worker.js +0 -43
  52. package/src/github.js +0 -139
  53. package/src/isolation-manager.js +0 -1381
  54. package/src/ledger.js +0 -614
  55. package/src/logic-engine.js +0 -223
  56. package/src/message-bus-bridge.js +0 -139
  57. package/src/message-bus.js +0 -249
  58. package/src/name-generator.js +0 -232
  59. package/src/orchestrator.js +0 -2519
  60. package/src/preflight.js +0 -485
  61. package/src/process-metrics.js +0 -566
  62. package/src/providers/anthropic/cli-builder.js +0 -45
  63. package/src/providers/anthropic/index.js +0 -134
  64. package/src/providers/anthropic/models.js +0 -23
  65. package/src/providers/anthropic/output-parser.js +0 -159
  66. package/src/providers/base-provider.js +0 -181
  67. package/src/providers/capabilities.js +0 -51
  68. package/src/providers/google/cli-builder.js +0 -55
  69. package/src/providers/google/index.js +0 -116
  70. package/src/providers/google/models.js +0 -24
  71. package/src/providers/google/output-parser.js +0 -92
  72. package/src/providers/index.js +0 -75
  73. package/src/providers/openai/cli-builder.js +0 -122
  74. package/src/providers/openai/index.js +0 -135
  75. package/src/providers/openai/models.js +0 -21
  76. package/src/providers/openai/output-parser.js +0 -129
  77. package/src/schemas/sub-cluster.js +0 -156
  78. package/src/status-footer.js +0 -980
  79. package/src/sub-cluster-wrapper.js +0 -560
  80. package/src/task-runner.js +0 -30
  81. package/src/template-resolver.js +0 -369
  82. package/src/tui/CHANGES.txt +0 -133
  83. package/src/tui/LAYOUT.md +0 -261
  84. package/src/tui/README.txt +0 -192
  85. package/src/tui/TWO-LEVEL-NAVIGATION.md +0 -186
  86. package/src/tui/data-poller.js +0 -325
  87. package/src/tui/demo.js +0 -208
  88. package/src/tui/formatters.js +0 -128
  89. package/src/tui/index.js +0 -193
  90. package/src/tui/keybindings.js +0 -383
  91. package/src/tui/layout.js +0 -334
  92. package/src/tui/renderer.js +0 -194
  93. package/task-lib/attachable-watcher.js +0 -272
  94. package/task-lib/claude-recovery.js +0 -119
  95. package/task-lib/commands/clean.js +0 -50
  96. package/task-lib/commands/get-log-path.js +0 -23
  97. package/task-lib/commands/kill.js +0 -32
  98. package/task-lib/commands/list.js +0 -105
  99. package/task-lib/commands/logs.js +0 -411
  100. package/task-lib/commands/resume.js +0 -42
  101. package/task-lib/commands/run.js +0 -57
  102. package/task-lib/commands/schedule.js +0 -105
  103. package/task-lib/commands/scheduler-cmd.js +0 -96
  104. package/task-lib/commands/schedules.js +0 -98
  105. package/task-lib/commands/status.js +0 -44
  106. package/task-lib/commands/unschedule.js +0 -16
  107. package/task-lib/completion.js +0 -9
  108. package/task-lib/config.js +0 -10
  109. package/task-lib/name-generator.js +0 -230
  110. package/task-lib/package.json +0 -3
  111. package/task-lib/runner.js +0 -144
  112. package/task-lib/scheduler.js +0 -252
  113. package/task-lib/store.js +0 -230
  114. package/task-lib/tui/formatters.js +0 -166
  115. package/task-lib/tui/index.js +0 -197
  116. package/task-lib/tui/layout.js +0 -111
  117. package/task-lib/tui/renderer.js +0 -119
  118. package/task-lib/tui.js +0 -384
  119. package/task-lib/watcher.js +0 -188
package/src/ledger.js DELETED
@@ -1,614 +0,0 @@
1
- /**
2
- * Ledger - Immutable event log for multi-agent coordination
3
- *
4
- * Provides:
5
- * - SQLite-backed message storage with indexes
6
- * - Query API for message retrieval
7
- * - In-memory cache for recent queries
8
- * - Subscription mechanism for real-time updates
9
- */
10
-
11
- const Database = require('better-sqlite3');
12
- const EventEmitter = require('events');
13
- const crypto = require('crypto');
14
-
15
- class Ledger extends EventEmitter {
16
- constructor(dbPath = ':memory:') {
17
- super();
18
- this.db = new Database(dbPath);
19
- this.cache = new Map(); // LRU cache for queries
20
- this.cacheLimit = 1000;
21
- this._closed = false; // Track closed state to prevent write-after-close
22
- this._lastTimestamp = 0;
23
- this._initSchema();
24
- }
25
-
26
- _initSchema() {
27
- // Enable WAL mode for concurrent reads
28
- this.db.pragma('journal_mode = WAL');
29
- // Force synchronous writes so other processes see changes immediately
30
- this.db.pragma('synchronous = NORMAL');
31
- // Checkpoint WAL frequently for cross-process visibility
32
- this.db.pragma('wal_autocheckpoint = 1');
33
-
34
- // Create messages table
35
- this.db.exec(`
36
- CREATE TABLE IF NOT EXISTS messages (
37
- id TEXT PRIMARY KEY,
38
- timestamp INTEGER NOT NULL,
39
- topic TEXT NOT NULL,
40
- sender TEXT NOT NULL,
41
- receiver TEXT NOT NULL,
42
- content_text TEXT,
43
- content_data TEXT,
44
- metadata TEXT,
45
- cluster_id TEXT NOT NULL
46
- );
47
-
48
- CREATE INDEX IF NOT EXISTS idx_timestamp ON messages(timestamp);
49
- CREATE INDEX IF NOT EXISTS idx_topic ON messages(topic);
50
- CREATE INDEX IF NOT EXISTS idx_cluster_sender ON messages(cluster_id, sender);
51
- CREATE INDEX IF NOT EXISTS idx_cluster_topic ON messages(cluster_id, topic);
52
- CREATE INDEX IF NOT EXISTS idx_cluster_timestamp ON messages(cluster_id, timestamp);
53
- `);
54
-
55
- this._prepareStatements();
56
- this._loadLastTimestamp();
57
- }
58
-
59
- _prepareStatements() {
60
- this.stmts = {
61
- insert: this.db.prepare(`
62
- INSERT INTO messages (id, timestamp, topic, sender, receiver, content_text, content_data, metadata, cluster_id)
63
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
64
- `),
65
-
66
- queryBase: `SELECT * FROM messages WHERE cluster_id = ?`,
67
-
68
- count: this.db.prepare(`SELECT COUNT(*) as count FROM messages WHERE cluster_id = ?`),
69
-
70
- getAll: this.db.prepare(`SELECT * FROM messages WHERE cluster_id = ? ORDER BY timestamp ASC`),
71
- };
72
- }
73
-
74
- _loadLastTimestamp() {
75
- const row = this.db.prepare('SELECT MAX(timestamp) AS max_timestamp FROM messages').get();
76
- if (row && Number.isFinite(row.max_timestamp)) {
77
- this._lastTimestamp = row.max_timestamp;
78
- }
79
- }
80
-
81
- /**
82
- * Append a message to the ledger
83
- * @param {Object} message - Message object
84
- * @returns {Object} The appended message with generated ID
85
- */
86
- append(message) {
87
- // Guard against write-after-close race condition
88
- // This can happen when orchestrator closes ledger while agents are still publishing
89
- if (this._closed) {
90
- // Silent return - agent is being stopped, message loss is expected
91
- return null;
92
- }
93
-
94
- const id = message.id || `msg_${crypto.randomBytes(16).toString('hex')}`;
95
- const baseTimestamp = Math.max(Date.now(), this._lastTimestamp + 1);
96
- const requestedTimestamp = typeof message.timestamp === 'number' ? message.timestamp : null;
97
- const timestamp =
98
- requestedTimestamp !== null ? Math.max(requestedTimestamp, baseTimestamp) : baseTimestamp;
99
-
100
- const record = {
101
- id,
102
- timestamp,
103
- topic: message.topic,
104
- sender: message.sender,
105
- receiver: message.receiver || 'broadcast',
106
- content_text: message.content?.text || null,
107
- content_data: message.content?.data ? JSON.stringify(message.content.data) : null,
108
- metadata: message.metadata ? JSON.stringify(message.metadata) : null,
109
- cluster_id: message.cluster_id,
110
- };
111
-
112
- try {
113
- this.stmts.insert.run(
114
- record.id,
115
- record.timestamp,
116
- record.topic,
117
- record.sender,
118
- record.receiver,
119
- record.content_text,
120
- record.content_data,
121
- record.metadata,
122
- record.cluster_id
123
- );
124
-
125
- // Invalidate cache
126
- this.cache.clear();
127
-
128
- this._lastTimestamp = Math.max(this._lastTimestamp, timestamp);
129
-
130
- // Emit event for subscriptions
131
- const fullMessage = this._deserializeMessage(record);
132
- this.emit('message', fullMessage);
133
- this.emit(`topic:${message.topic}`, fullMessage);
134
-
135
- return fullMessage;
136
- } catch (error) {
137
- throw new Error(`Failed to append message: ${error.message}`);
138
- }
139
- }
140
-
141
- /**
142
- * Append multiple messages atomically using a transaction
143
- * All messages get contiguous timestamps and are committed together.
144
- * If any insert fails, the entire batch is rolled back.
145
- *
146
- * Use this for task completion messages to prevent interleaving:
147
- * - TOKEN_USAGE, TASK_COMPLETED, and hook messages published atomically
148
- * - Other agents' messages cannot appear between them
149
- *
150
- * @param {Array<Object>} messages - Array of message objects
151
- * @returns {Array<Object>} Array of appended messages with generated IDs
152
- */
153
- batchAppend(messages) {
154
- if (!Array.isArray(messages) || messages.length === 0) {
155
- return [];
156
- }
157
-
158
- // Guard against write-after-close race condition
159
- if (this._closed) {
160
- return [];
161
- }
162
-
163
- // Create transaction function - all inserts happen atomically
164
- const insertMany = this.db.transaction((msgs) => {
165
- const results = [];
166
- const baseTimestamp = Math.max(Date.now(), this._lastTimestamp + 1);
167
-
168
- for (let i = 0; i < msgs.length; i++) {
169
- const message = msgs[i];
170
- const id = message.id || `msg_${crypto.randomBytes(16).toString('hex')}`;
171
- // Use incrementing timestamps to preserve order within batch
172
- const timestamp = baseTimestamp + i;
173
-
174
- const record = {
175
- id,
176
- timestamp,
177
- topic: message.topic,
178
- sender: message.sender,
179
- receiver: message.receiver || 'broadcast',
180
- content_text: message.content?.text || null,
181
- content_data: message.content?.data ? JSON.stringify(message.content.data) : null,
182
- metadata: message.metadata ? JSON.stringify(message.metadata) : null,
183
- cluster_id: message.cluster_id,
184
- };
185
-
186
- this.stmts.insert.run(
187
- record.id,
188
- record.timestamp,
189
- record.topic,
190
- record.sender,
191
- record.receiver,
192
- record.content_text,
193
- record.content_data,
194
- record.metadata,
195
- record.cluster_id
196
- );
197
-
198
- results.push(this._deserializeMessage(record));
199
- }
200
-
201
- return { results, baseTimestamp };
202
- });
203
-
204
- try {
205
- // Execute transaction (atomic - all or nothing)
206
- const { results: appendedMessages, baseTimestamp } = insertMany(messages);
207
-
208
- // Invalidate cache
209
- this.cache.clear();
210
-
211
- this._lastTimestamp = Math.max(this._lastTimestamp, baseTimestamp + messages.length - 1);
212
-
213
- // Emit events for subscriptions AFTER transaction commits
214
- // This ensures listeners see consistent state
215
- for (const fullMessage of appendedMessages) {
216
- this.emit('message', fullMessage);
217
- this.emit(`topic:${fullMessage.topic}`, fullMessage);
218
- }
219
-
220
- return appendedMessages;
221
- } catch (error) {
222
- throw new Error(`Failed to batch append messages: ${error.message}`);
223
- }
224
- }
225
-
226
- /**
227
- * Query messages with filters
228
- * @param {Object} criteria - Query criteria
229
- * @returns {Array} Matching messages
230
- */
231
- query(criteria) {
232
- const { cluster_id, topic, sender, receiver, since, until, limit, offset } = criteria;
233
-
234
- if (!cluster_id) {
235
- throw new Error('cluster_id is required for queries');
236
- }
237
-
238
- // Build query
239
- const conditions = ['cluster_id = ?'];
240
- const params = [cluster_id];
241
-
242
- if (topic) {
243
- conditions.push('topic = ?');
244
- params.push(topic);
245
- }
246
-
247
- if (sender) {
248
- conditions.push('sender = ?');
249
- params.push(sender);
250
- }
251
-
252
- if (receiver) {
253
- conditions.push('receiver = ?');
254
- params.push(receiver);
255
- }
256
-
257
- if (since) {
258
- conditions.push('timestamp >= ?');
259
- params.push(typeof since === 'number' ? since : new Date(since).getTime());
260
- }
261
-
262
- if (until) {
263
- conditions.push('timestamp <= ?');
264
- params.push(typeof until === 'number' ? until : new Date(until).getTime());
265
- }
266
-
267
- // Defend against prototype pollution affecting default query ordering.
268
- // Only treat `criteria.order` as set if it's an own property.
269
- const orderValue = Object.prototype.hasOwnProperty.call(criteria, 'order')
270
- ? criteria.order
271
- : undefined;
272
- const direction = String(orderValue ?? 'asc').toLowerCase() === 'desc' ? 'DESC' : 'ASC';
273
- let sql = `SELECT * FROM messages WHERE ${conditions.join(' AND ')} ORDER BY timestamp ${direction}`;
274
-
275
- if (limit) {
276
- sql += ` LIMIT ?`;
277
- params.push(limit);
278
- }
279
-
280
- if (offset) {
281
- sql += ` OFFSET ?`;
282
- params.push(offset);
283
- }
284
-
285
- const stmt = this.db.prepare(sql);
286
- const rows = stmt.all(...params);
287
- return rows.map((row) => this._deserializeMessage(row));
288
- }
289
-
290
- /**
291
- * Find the last message matching criteria
292
- * @param {Object} criteria - Query criteria
293
- * @returns {Object|null} Last matching message
294
- */
295
- findLast(criteria) {
296
- const { cluster_id, topic, sender, receiver, since, until } = criteria;
297
-
298
- if (!cluster_id) {
299
- throw new Error('cluster_id is required for queries');
300
- }
301
-
302
- // Build query with DESC order
303
- const conditions = ['cluster_id = ?'];
304
- const params = [cluster_id];
305
-
306
- if (topic) {
307
- conditions.push('topic = ?');
308
- params.push(topic);
309
- }
310
-
311
- if (sender) {
312
- conditions.push('sender = ?');
313
- params.push(sender);
314
- }
315
-
316
- if (receiver) {
317
- conditions.push('receiver = ?');
318
- params.push(receiver);
319
- }
320
-
321
- if (since) {
322
- conditions.push('timestamp >= ?');
323
- params.push(typeof since === 'number' ? since : new Date(since).getTime());
324
- }
325
-
326
- if (until) {
327
- conditions.push('timestamp <= ?');
328
- params.push(typeof until === 'number' ? until : new Date(until).getTime());
329
- }
330
-
331
- const sql = `SELECT * FROM messages WHERE ${conditions.join(' AND ')} ORDER BY timestamp DESC LIMIT 1`;
332
-
333
- const stmt = this.db.prepare(sql);
334
- const row = stmt.get(...params);
335
- return row ? this._deserializeMessage(row) : null;
336
- }
337
-
338
- /**
339
- * Count messages matching criteria
340
- * @param {Object} criteria - Query criteria
341
- * @returns {Number} Message count
342
- */
343
- count(criteria) {
344
- const { cluster_id, topic } = criteria;
345
-
346
- if (!cluster_id) {
347
- throw new Error('cluster_id is required for count');
348
- }
349
-
350
- let sql = 'SELECT COUNT(*) as count FROM messages WHERE cluster_id = ?';
351
- const params = [cluster_id];
352
-
353
- if (topic) {
354
- sql += ' AND topic = ?';
355
- params.push(topic);
356
- }
357
-
358
- const stmt = this.db.prepare(sql);
359
- const result = stmt.get(...params);
360
- return result.count;
361
- }
362
-
363
- /**
364
- * Get messages since a specific timestamp
365
- * @param {Object} params - { cluster_id, timestamp }
366
- * @returns {Array} Messages since timestamp
367
- */
368
- since(params) {
369
- return this.query({
370
- cluster_id: params.cluster_id,
371
- since: params.timestamp,
372
- });
373
- }
374
-
375
- /**
376
- * Get all messages for a cluster
377
- * @param {String} cluster_id - Cluster ID
378
- * @returns {Array} All messages
379
- */
380
- getAll(cluster_id) {
381
- const rows = this.stmts.getAll.all(cluster_id);
382
- return rows.map((row) => this._deserializeMessage(row));
383
- }
384
-
385
- /**
386
- * Get aggregated token usage by agent role
387
- * Queries TOKEN_USAGE messages and sums tokens per role
388
- * @param {String} cluster_id - Cluster ID
389
- * @returns {Object} Token usage aggregated by role
390
- * Example: {
391
- * implementation: { inputTokens: 5000, outputTokens: 2000, totalCostUsd: 0.05, count: 3 },
392
- * validator: { inputTokens: 3000, outputTokens: 1500, totalCostUsd: 0.03, count: 2 },
393
- * _total: { inputTokens: 8000, outputTokens: 3500, totalCostUsd: 0.08, count: 5 }
394
- * }
395
- */
396
- getTokensByRole(cluster_id) {
397
- if (!cluster_id) {
398
- throw new Error('cluster_id is required for getTokensByRole');
399
- }
400
-
401
- // Query all TOKEN_USAGE messages for this cluster
402
- const sql = `SELECT * FROM messages WHERE cluster_id = ? AND topic = 'TOKEN_USAGE' ORDER BY timestamp ASC`;
403
- const stmt = this.db.prepare(sql);
404
- const rows = stmt.all(cluster_id);
405
-
406
- const byRole = {};
407
- const total = {
408
- inputTokens: 0,
409
- outputTokens: 0,
410
- cacheReadInputTokens: 0,
411
- cacheCreationInputTokens: 0,
412
- totalCostUsd: 0,
413
- count: 0,
414
- };
415
-
416
- for (const row of rows) {
417
- const message = this._deserializeMessage(row);
418
- const data = message.content?.data || {};
419
- const role = data.role || 'unknown';
420
-
421
- // Initialize role bucket if needed
422
- if (!byRole[role]) {
423
- byRole[role] = {
424
- inputTokens: 0,
425
- outputTokens: 0,
426
- cacheReadInputTokens: 0,
427
- cacheCreationInputTokens: 0,
428
- totalCostUsd: 0,
429
- count: 0,
430
- };
431
- }
432
-
433
- // Aggregate tokens for this role
434
- byRole[role].inputTokens += data.inputTokens || 0;
435
- byRole[role].outputTokens += data.outputTokens || 0;
436
- byRole[role].cacheReadInputTokens += data.cacheReadInputTokens || 0;
437
- byRole[role].cacheCreationInputTokens += data.cacheCreationInputTokens || 0;
438
- byRole[role].totalCostUsd += data.totalCostUsd || 0;
439
- byRole[role].count += 1;
440
-
441
- // Aggregate totals
442
- total.inputTokens += data.inputTokens || 0;
443
- total.outputTokens += data.outputTokens || 0;
444
- total.cacheReadInputTokens += data.cacheReadInputTokens || 0;
445
- total.cacheCreationInputTokens += data.cacheCreationInputTokens || 0;
446
- total.totalCostUsd += data.totalCostUsd || 0;
447
- total.count += 1;
448
- }
449
-
450
- // Add total as special _total key
451
- byRole._total = total;
452
-
453
- return byRole;
454
- }
455
-
456
- /**
457
- * Subscribe to new messages
458
- * @param {Function} callback - Called with each new message
459
- * @returns {Function} Unsubscribe function
460
- */
461
- subscribe(callback) {
462
- this.on('message', callback);
463
- return () => this.off('message', callback);
464
- }
465
-
466
- /**
467
- * Poll for new messages (cross-process support)
468
- * @param {String} clusterId - Cluster ID to poll (null for all clusters)
469
- * @param {Function} callback - Called with each new message
470
- * @param {Number} intervalMs - Poll interval (default 500ms)
471
- * @param {Number} initialCount - Number of messages to show initially (default 300)
472
- * @returns {Function} Stop polling function
473
- */
474
- pollForMessages(clusterId, callback, intervalMs = 500, initialCount = 300) {
475
- let lastTimestamp = 0;
476
- let lastMessageIds = new Set();
477
- let isFirstPoll = true;
478
-
479
- const poll = () => {
480
- try {
481
- let sql, params;
482
-
483
- if (isFirstPoll) {
484
- // First poll: get last N messages by count
485
- if (clusterId) {
486
- sql =
487
- 'SELECT * FROM (SELECT * FROM messages WHERE cluster_id = ? ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC';
488
- params = [clusterId, initialCount];
489
- } else {
490
- sql =
491
- 'SELECT * FROM (SELECT * FROM messages ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC';
492
- params = [initialCount];
493
- }
494
- isFirstPoll = false;
495
- } else {
496
- // Subsequent polls: get messages since last timestamp
497
- if (clusterId) {
498
- sql =
499
- 'SELECT * FROM messages WHERE cluster_id = ? AND timestamp >= ? ORDER BY timestamp ASC';
500
- params = [clusterId, lastTimestamp - 1000]; // 1s buffer for race conditions
501
- } else {
502
- sql = 'SELECT * FROM messages WHERE timestamp >= ? ORDER BY timestamp ASC';
503
- params = [lastTimestamp - 1000];
504
- }
505
- }
506
-
507
- const stmt = this.db.prepare(sql);
508
- const rows = stmt.all(...params);
509
-
510
- for (const row of rows) {
511
- // Skip already-seen messages
512
- if (lastMessageIds.has(row.id)) continue;
513
-
514
- lastMessageIds.add(row.id);
515
- const message = this._deserializeMessage(row);
516
- callback(message);
517
-
518
- // Update timestamp high-water mark
519
- if (row.timestamp > lastTimestamp) {
520
- lastTimestamp = row.timestamp;
521
- }
522
- }
523
-
524
- // Prune old message IDs to prevent memory leak
525
- if (lastMessageIds.size > 10000) {
526
- const idsArray = Array.from(lastMessageIds);
527
- lastMessageIds = new Set(idsArray.slice(-5000));
528
- }
529
- } catch (error) {
530
- // DB busy is expected during concurrent access - log but continue polling
531
- // Other errors indicate real bugs and should be visible
532
- console.error(`[Ledger] pollForMessages error (will retry): ${error.message}`);
533
- }
534
- };
535
-
536
- // Initial poll
537
- poll();
538
-
539
- // Set up interval
540
- const intervalId = setInterval(poll, intervalMs);
541
-
542
- // Return stop function
543
- return () => clearInterval(intervalId);
544
- }
545
-
546
- /**
547
- * Subscribe to specific topic
548
- * @param {String} topic - Topic to subscribe to
549
- * @param {Function} callback - Called with matching messages
550
- * @returns {Function} Unsubscribe function
551
- */
552
- subscribeTopic(topic, callback) {
553
- const event = `topic:${topic}`;
554
- this.on(event, callback);
555
- return () => this.off(event, callback);
556
- }
557
-
558
- /**
559
- * Deserialize a database row into a message object
560
- * @private
561
- */
562
- _deserializeMessage(row) {
563
- const message = {
564
- id: row.id,
565
- timestamp: row.timestamp,
566
- topic: row.topic,
567
- sender: row.sender,
568
- receiver: row.receiver,
569
- cluster_id: row.cluster_id,
570
- };
571
-
572
- if (row.content_text || row.content_data) {
573
- message.content = {};
574
- if (row.content_text) {
575
- message.content.text = row.content_text;
576
- }
577
- if (row.content_data) {
578
- try {
579
- message.content.data = JSON.parse(row.content_data);
580
- } catch {
581
- message.content.data = null;
582
- }
583
- }
584
- }
585
-
586
- if (row.metadata) {
587
- try {
588
- message.metadata = JSON.parse(row.metadata);
589
- } catch {
590
- message.metadata = null;
591
- }
592
- }
593
-
594
- return message;
595
- }
596
-
597
- /**
598
- * Close the database connection
599
- */
600
- close() {
601
- this._closed = true; // Set flag BEFORE closing to prevent race conditions
602
- this.db.close();
603
- }
604
-
605
- /**
606
- * Clear all messages (for testing)
607
- */
608
- clear() {
609
- this.db.exec('DELETE FROM messages');
610
- this.cache.clear();
611
- }
612
- }
613
-
614
- module.exports = Ledger;