@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.
- package/LICENSE +1 -1
- package/README.md +8 -385
- package/cli/index.js +108 -4615
- package/package.json +16 -125
- package/CHANGELOG.md +0 -537
- package/cli/commands/providers.js +0 -150
- package/cli/lib/first-run.js +0 -211
- package/cli/lib/update-checker.js +0 -281
- package/cli/message-formatter-utils.js +0 -75
- package/cli/message-formatters-normal.js +0 -253
- package/cli/message-formatters-watch.js +0 -181
- package/cluster-templates/base-templates/debug-workflow.json +0 -375
- package/cluster-templates/base-templates/full-workflow.json +0 -697
- package/cluster-templates/base-templates/single-worker.json +0 -63
- package/cluster-templates/base-templates/worker-validator.json +0 -152
- package/cluster-templates/conductor-bootstrap.json +0 -124
- package/docker/zeroshot-cluster/Dockerfile +0 -152
- package/docker/zeroshot-cluster/pre-baked-deps.json +0 -28
- package/lib/completion.js +0 -174
- package/lib/docker-config.js +0 -212
- package/lib/id-detector.js +0 -53
- package/lib/provider-detection.js +0 -59
- package/lib/provider-names.js +0 -56
- package/lib/settings.js +0 -405
- package/lib/stream-json-parser.js +0 -10
- package/scripts/fix-node-pty-permissions.js +0 -75
- package/scripts/record-demo.sh +0 -279
- package/scripts/test-install.sh +0 -40
- package/scripts/validate-templates.js +0 -100
- package/src/agent/agent-config.js +0 -193
- package/src/agent/agent-context-builder.js +0 -335
- package/src/agent/agent-hook-executor.js +0 -402
- package/src/agent/agent-lifecycle.js +0 -683
- package/src/agent/agent-stuck-detector.js +0 -256
- package/src/agent/agent-task-executor.js +0 -1431
- package/src/agent/agent-trigger-evaluator.js +0 -67
- package/src/agent/output-extraction.js +0 -200
- package/src/agent/output-reformatter.js +0 -175
- package/src/agent/schema-utils.js +0 -111
- package/src/agent-wrapper.js +0 -570
- package/src/agents/git-pusher-agent.json +0 -20
- package/src/attach/attach-client.js +0 -438
- package/src/attach/attach-server.js +0 -543
- package/src/attach/index.js +0 -35
- package/src/attach/protocol.js +0 -220
- package/src/attach/ring-buffer.js +0 -121
- package/src/attach/socket-discovery.js +0 -267
- package/src/claude-task-runner.js +0 -523
- package/src/config-router.js +0 -80
- package/src/config-validator.js +0 -1660
- package/src/copy-worker.js +0 -43
- package/src/github.js +0 -139
- package/src/isolation-manager.js +0 -1381
- package/src/ledger.js +0 -614
- package/src/logic-engine.js +0 -223
- package/src/message-bus-bridge.js +0 -139
- package/src/message-bus.js +0 -249
- package/src/name-generator.js +0 -232
- package/src/orchestrator.js +0 -2519
- package/src/preflight.js +0 -485
- package/src/process-metrics.js +0 -566
- package/src/providers/anthropic/cli-builder.js +0 -45
- package/src/providers/anthropic/index.js +0 -134
- package/src/providers/anthropic/models.js +0 -23
- package/src/providers/anthropic/output-parser.js +0 -159
- package/src/providers/base-provider.js +0 -181
- package/src/providers/capabilities.js +0 -51
- package/src/providers/google/cli-builder.js +0 -55
- package/src/providers/google/index.js +0 -116
- package/src/providers/google/models.js +0 -24
- package/src/providers/google/output-parser.js +0 -92
- package/src/providers/index.js +0 -75
- package/src/providers/openai/cli-builder.js +0 -122
- package/src/providers/openai/index.js +0 -135
- package/src/providers/openai/models.js +0 -21
- package/src/providers/openai/output-parser.js +0 -129
- package/src/schemas/sub-cluster.js +0 -156
- package/src/status-footer.js +0 -980
- package/src/sub-cluster-wrapper.js +0 -560
- package/src/task-runner.js +0 -30
- package/src/template-resolver.js +0 -369
- package/src/tui/CHANGES.txt +0 -133
- package/src/tui/LAYOUT.md +0 -261
- package/src/tui/README.txt +0 -192
- package/src/tui/TWO-LEVEL-NAVIGATION.md +0 -186
- package/src/tui/data-poller.js +0 -325
- package/src/tui/demo.js +0 -208
- package/src/tui/formatters.js +0 -128
- package/src/tui/index.js +0 -193
- package/src/tui/keybindings.js +0 -383
- package/src/tui/layout.js +0 -334
- package/src/tui/renderer.js +0 -194
- package/task-lib/attachable-watcher.js +0 -272
- package/task-lib/claude-recovery.js +0 -119
- package/task-lib/commands/clean.js +0 -50
- package/task-lib/commands/get-log-path.js +0 -23
- package/task-lib/commands/kill.js +0 -32
- package/task-lib/commands/list.js +0 -105
- package/task-lib/commands/logs.js +0 -411
- package/task-lib/commands/resume.js +0 -42
- package/task-lib/commands/run.js +0 -57
- package/task-lib/commands/schedule.js +0 -105
- package/task-lib/commands/scheduler-cmd.js +0 -96
- package/task-lib/commands/schedules.js +0 -98
- package/task-lib/commands/status.js +0 -44
- package/task-lib/commands/unschedule.js +0 -16
- package/task-lib/completion.js +0 -9
- package/task-lib/config.js +0 -10
- package/task-lib/name-generator.js +0 -230
- package/task-lib/package.json +0 -3
- package/task-lib/runner.js +0 -144
- package/task-lib/scheduler.js +0 -252
- package/task-lib/store.js +0 -230
- package/task-lib/tui/formatters.js +0 -166
- package/task-lib/tui/index.js +0 -197
- package/task-lib/tui/layout.js +0 -111
- package/task-lib/tui/renderer.js +0 -119
- package/task-lib/tui.js +0 -384
- 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;
|