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