@the-open-engine/zeroshot 6.34.1 → 6.34.2
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/cli/index.js +34 -8
- package/cli/json-export.js +77 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/agent/agent-task-executor.js +380 -99
- package/src/ledger.js +364 -31
- package/src/template-resolver.js +5 -0
- package/task-lib/watcher-output-runtime.js +128 -43
package/src/ledger.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Ledger -
|
|
2
|
+
* Ledger - Durable event log for multi-agent coordination
|
|
3
3
|
*
|
|
4
4
|
* Provides:
|
|
5
|
+
* - Immutable control/replayable messages plus a bounded newest tail of raw AGENT_OUTPUT
|
|
5
6
|
* - SQLite-backed message storage with indexes
|
|
6
7
|
* - Query API for message retrieval
|
|
7
8
|
* - In-memory cache for recent queries
|
|
@@ -16,9 +17,14 @@ const {
|
|
|
16
17
|
USER_GUIDANCE_AGENT,
|
|
17
18
|
USER_GUIDANCE_CLUSTER,
|
|
18
19
|
} = require('./guidance-topics');
|
|
20
|
+
const { isReplayableMessage } = require('./agent/context-replay-policy');
|
|
19
21
|
const { messageSequenceFromSql, messageSequenceToSql } = require('./ledger-sequence');
|
|
20
22
|
|
|
21
23
|
const MESSAGE_SEQUENCE_SELECT = 'CAST(rowid AS TEXT) AS sequence';
|
|
24
|
+
const AGENT_OUTPUT_EXPORT_MAX_BYTES = 8 * 1024 * 1024;
|
|
25
|
+
const AGENT_OUTPUT_EXPORT_MAX_MESSAGES = 8192;
|
|
26
|
+
const EMPTY_OMISSION_DIGEST = '0'.repeat(64);
|
|
27
|
+
const AGENT_OUTPUT_RECEIPT_SENDER = 'zeroshot';
|
|
22
28
|
|
|
23
29
|
class Ledger extends EventEmitter {
|
|
24
30
|
constructor(dbPath = ':memory:', options = {}) {
|
|
@@ -87,16 +93,40 @@ class Ledger extends EventEmitter {
|
|
|
87
93
|
CREATE INDEX IF NOT EXISTS idx_cluster_sender ON messages(cluster_id, sender);
|
|
88
94
|
CREATE INDEX IF NOT EXISTS idx_cluster_topic ON messages(cluster_id, topic);
|
|
89
95
|
CREATE INDEX IF NOT EXISTS idx_cluster_timestamp ON messages(cluster_id, timestamp);
|
|
96
|
+
|
|
97
|
+
CREATE TABLE IF NOT EXISTS agent_output_compaction (
|
|
98
|
+
cluster_id TEXT PRIMARY KEY,
|
|
99
|
+
receipt_message_id TEXT,
|
|
100
|
+
first_omitted_timestamp INTEGER,
|
|
101
|
+
omitted_messages INTEGER NOT NULL DEFAULT 0,
|
|
102
|
+
omitted_export_bytes INTEGER NOT NULL DEFAULT 0,
|
|
103
|
+
omission_digest TEXT NOT NULL,
|
|
104
|
+
retained_messages INTEGER NOT NULL DEFAULT 0,
|
|
105
|
+
retained_export_bytes INTEGER NOT NULL DEFAULT 0
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
CREATE TABLE IF NOT EXISTS ledger_sequence (
|
|
109
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
110
|
+
value INTEGER NOT NULL
|
|
111
|
+
);
|
|
112
|
+
INSERT OR IGNORE INTO ledger_sequence (singleton, value) VALUES (1, 0);
|
|
113
|
+
UPDATE ledger_sequence
|
|
114
|
+
SET value = MAX(value, COALESCE((SELECT MAX(rowid) FROM messages), 0))
|
|
115
|
+
WHERE singleton = 1;
|
|
90
116
|
`);
|
|
91
117
|
|
|
92
118
|
this._prepareStatements();
|
|
93
119
|
this._loadLastTimestamp();
|
|
120
|
+
this._reconcileAgentOutput();
|
|
94
121
|
}
|
|
95
122
|
|
|
96
123
|
_prepareStatements() {
|
|
97
124
|
const insert = this.db.prepare(`
|
|
98
|
-
INSERT INTO messages (
|
|
99
|
-
|
|
125
|
+
INSERT INTO messages (
|
|
126
|
+
rowid, id, timestamp, topic, sender, receiver,
|
|
127
|
+
content_text, content_data, metadata, cluster_id
|
|
128
|
+
)
|
|
129
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
100
130
|
`);
|
|
101
131
|
insert.safeIntegers(true);
|
|
102
132
|
|
|
@@ -111,9 +141,94 @@ class Ledger extends EventEmitter {
|
|
|
111
141
|
`SELECT ${MESSAGE_SEQUENCE_SELECT}, * FROM messages WHERE cluster_id = ?` +
|
|
112
142
|
` ORDER BY timestamp ASC, rowid ASC`
|
|
113
143
|
),
|
|
144
|
+
|
|
145
|
+
agentOutputRows: this.db.prepare(
|
|
146
|
+
`SELECT ${MESSAGE_SEQUENCE_SELECT}, * FROM messages` +
|
|
147
|
+
` WHERE cluster_id = ? AND topic = 'AGENT_OUTPUT' ORDER BY timestamp ASC, rowid ASC`
|
|
148
|
+
),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
if (!this.readonly) {
|
|
152
|
+
const nextSequence = this.db.prepare(`
|
|
153
|
+
UPDATE ledger_sequence
|
|
154
|
+
SET value = MAX(value, COALESCE((SELECT MAX(rowid) FROM messages), 0)) + 1
|
|
155
|
+
WHERE singleton = 1
|
|
156
|
+
AND value < 9223372036854775807
|
|
157
|
+
AND COALESCE((SELECT MAX(rowid) FROM messages), 0) < 9223372036854775807
|
|
158
|
+
RETURNING value
|
|
159
|
+
`);
|
|
160
|
+
nextSequence.safeIntegers(true);
|
|
161
|
+
this.stmts.nextSequence = nextSequence;
|
|
162
|
+
this._prepareAgentOutputCompactionStatements();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
_prepareAgentOutputCompactionStatements() {
|
|
167
|
+
this.compactionStmts = {
|
|
168
|
+
get: this.db.prepare('SELECT * FROM agent_output_compaction WHERE cluster_id = ?'),
|
|
169
|
+
insert: this.db.prepare(`
|
|
170
|
+
INSERT INTO agent_output_compaction (
|
|
171
|
+
cluster_id, receipt_message_id, first_omitted_timestamp,
|
|
172
|
+
omitted_messages, omitted_export_bytes, omission_digest,
|
|
173
|
+
retained_messages, retained_export_bytes
|
|
174
|
+
) VALUES (?, NULL, NULL, 0, 0, ?, ?, ?)
|
|
175
|
+
`),
|
|
176
|
+
update: this.db.prepare(`
|
|
177
|
+
UPDATE agent_output_compaction
|
|
178
|
+
SET receipt_message_id = ?, first_omitted_timestamp = ?, omitted_messages = ?,
|
|
179
|
+
omitted_export_bytes = ?, omission_digest = ?, retained_messages = ?,
|
|
180
|
+
retained_export_bytes = ?
|
|
181
|
+
WHERE cluster_id = ?
|
|
182
|
+
`),
|
|
183
|
+
deleteMessage: this.db.prepare('DELETE FROM messages WHERE id = ?'),
|
|
184
|
+
updateReceipt: this.db.prepare(`
|
|
185
|
+
UPDATE messages SET content_data = ?, metadata = ? WHERE id = ?
|
|
186
|
+
`),
|
|
187
|
+
receiptExists: this.db.prepare('SELECT 1 FROM messages WHERE id = ?'),
|
|
188
|
+
agentOutputClusterIds: this.db.prepare(`
|
|
189
|
+
SELECT cluster_id FROM agent_output_compaction
|
|
190
|
+
UNION
|
|
191
|
+
SELECT cluster_id FROM messages WHERE topic = 'AGENT_OUTPUT'
|
|
192
|
+
`),
|
|
193
|
+
deleteState: this.db.prepare('DELETE FROM agent_output_compaction'),
|
|
114
194
|
};
|
|
115
195
|
}
|
|
116
196
|
|
|
197
|
+
_reconcileAgentOutput() {
|
|
198
|
+
const clusterIds = this.compactionStmts.agentOutputClusterIds
|
|
199
|
+
.all()
|
|
200
|
+
.map((row) => row.cluster_id);
|
|
201
|
+
const reconcileCluster = this.db.transaction((clusterId) => {
|
|
202
|
+
const measured = this._measureCompactableAgentOutput(clusterId);
|
|
203
|
+
let state = this.compactionStmts.get.get(clusterId);
|
|
204
|
+
if (!state) {
|
|
205
|
+
if (measured.retained_messages === 0) return;
|
|
206
|
+
state = this._createCompactionState(clusterId, measured);
|
|
207
|
+
} else if (
|
|
208
|
+
state.retained_messages !== measured.retained_messages ||
|
|
209
|
+
state.retained_export_bytes !== measured.retained_export_bytes
|
|
210
|
+
) {
|
|
211
|
+
state.retained_messages = measured.retained_messages;
|
|
212
|
+
state.retained_export_bytes = measured.retained_export_bytes;
|
|
213
|
+
this._updateCompactionState(state);
|
|
214
|
+
}
|
|
215
|
+
const receiptMissing =
|
|
216
|
+
state.omitted_messages > 0 &&
|
|
217
|
+
(!state.receipt_message_id ||
|
|
218
|
+
!this.compactionStmts.receiptExists.get(state.receipt_message_id));
|
|
219
|
+
if (
|
|
220
|
+
receiptMissing ||
|
|
221
|
+
state.retained_export_bytes > AGENT_OUTPUT_EXPORT_MAX_BYTES ||
|
|
222
|
+
state.retained_messages > AGENT_OUTPUT_EXPORT_MAX_MESSAGES
|
|
223
|
+
) {
|
|
224
|
+
this._compactAgentOutput(clusterId);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
for (const clusterId of clusterIds) {
|
|
228
|
+
reconcileCluster.immediate(clusterId);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
117
232
|
_loadLastTimestamp() {
|
|
118
233
|
const row = this.db.prepare('SELECT MAX(timestamp) AS max_timestamp FROM messages').get();
|
|
119
234
|
if (row && Number.isFinite(row.max_timestamp)) {
|
|
@@ -121,6 +236,191 @@ class Ledger extends EventEmitter {
|
|
|
121
236
|
}
|
|
122
237
|
}
|
|
123
238
|
|
|
239
|
+
_insertRecord(record) {
|
|
240
|
+
const allocated = this.stmts.nextSequence.get();
|
|
241
|
+
if (!allocated) {
|
|
242
|
+
throw new RangeError('Ledger message sequence exhausted the SQLite rowid range');
|
|
243
|
+
}
|
|
244
|
+
this.stmts.insert.run(
|
|
245
|
+
allocated.value,
|
|
246
|
+
record.id,
|
|
247
|
+
record.timestamp,
|
|
248
|
+
record.topic,
|
|
249
|
+
record.sender,
|
|
250
|
+
record.receiver,
|
|
251
|
+
record.content_text,
|
|
252
|
+
record.content_data,
|
|
253
|
+
record.metadata,
|
|
254
|
+
record.cluster_id
|
|
255
|
+
);
|
|
256
|
+
record.sequence = messageSequenceFromSql(allocated.value);
|
|
257
|
+
return this._deserializeMessage(record);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
_recordExportBytes(record) {
|
|
261
|
+
return Buffer.byteLength(JSON.stringify(this._deserializeMessage(record)));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
_isCompactableAgentOutput(record) {
|
|
265
|
+
if (record.topic !== 'AGENT_OUTPUT') return false;
|
|
266
|
+
const message = this._deserializeMessage(record);
|
|
267
|
+
return message.metadata?.compactionReceipt !== true && !isReplayableMessage(message);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
_measureCompactableAgentOutput(clusterId) {
|
|
271
|
+
let retainedMessages = 0;
|
|
272
|
+
let retainedExportBytes = 0;
|
|
273
|
+
for (const row of this.stmts.agentOutputRows.iterate(clusterId)) {
|
|
274
|
+
if (!this._isCompactableAgentOutput(row)) continue;
|
|
275
|
+
retainedMessages += 1;
|
|
276
|
+
retainedExportBytes += this._recordExportBytes(row);
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
retained_messages: retainedMessages,
|
|
280
|
+
retained_export_bytes: retainedExportBytes,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
_createCompactionState(clusterId, measured = this._measureCompactableAgentOutput(clusterId)) {
|
|
285
|
+
this.compactionStmts.insert.run(
|
|
286
|
+
clusterId,
|
|
287
|
+
EMPTY_OMISSION_DIGEST,
|
|
288
|
+
measured.retained_messages,
|
|
289
|
+
measured.retained_export_bytes
|
|
290
|
+
);
|
|
291
|
+
return this.compactionStmts.get.get(clusterId);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
_updateCompactionState(state) {
|
|
295
|
+
this.compactionStmts.update.run(
|
|
296
|
+
state.receipt_message_id,
|
|
297
|
+
state.first_omitted_timestamp,
|
|
298
|
+
state.omitted_messages,
|
|
299
|
+
state.omitted_export_bytes,
|
|
300
|
+
state.omission_digest,
|
|
301
|
+
state.retained_messages,
|
|
302
|
+
state.retained_export_bytes,
|
|
303
|
+
state.cluster_id
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
_oldestCompactableAgentOutput(clusterId) {
|
|
308
|
+
for (const row of this.stmts.agentOutputRows.iterate(clusterId)) {
|
|
309
|
+
if (this._isCompactableAgentOutput(row)) return row;
|
|
310
|
+
}
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
_nextOmissionDigest(previousDigest, record) {
|
|
315
|
+
const hash = crypto.createHash('sha256');
|
|
316
|
+
hash.update(Buffer.from(previousDigest, 'hex'));
|
|
317
|
+
const exportedMessage = Buffer.from(JSON.stringify(this._deserializeMessage(record)));
|
|
318
|
+
const length = Buffer.allocUnsafe(8);
|
|
319
|
+
length.writeBigUInt64BE(BigInt(exportedMessage.length));
|
|
320
|
+
hash.update(length);
|
|
321
|
+
hash.update(exportedMessage);
|
|
322
|
+
return hash.digest('hex');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
_agentOutputReceiptRecord(state) {
|
|
326
|
+
const line =
|
|
327
|
+
`[ZEROSHOT] Earlier raw provider output omitted from the durable control-plane tail ` +
|
|
328
|
+
`(messages=${state.omitted_messages}, export_bytes=${state.omitted_export_bytes}, ` +
|
|
329
|
+
`sha256_chain=${state.omission_digest}). The newest output is retained within ` +
|
|
330
|
+
`${AGENT_OUTPUT_EXPORT_MAX_BYTES} exported bytes and ` +
|
|
331
|
+
`${AGENT_OUTPUT_EXPORT_MAX_MESSAGES} messages. Complete output remains in task logs.`;
|
|
332
|
+
return {
|
|
333
|
+
id:
|
|
334
|
+
state.receipt_message_id ||
|
|
335
|
+
`agent_output_receipt_${crypto.createHash('sha256').update(state.cluster_id).digest('hex').slice(0, 24)}`,
|
|
336
|
+
timestamp: state.first_omitted_timestamp,
|
|
337
|
+
topic: 'AGENT_OUTPUT',
|
|
338
|
+
sender: AGENT_OUTPUT_RECEIPT_SENDER,
|
|
339
|
+
receiver: 'broadcast',
|
|
340
|
+
content_text: null,
|
|
341
|
+
content_data: JSON.stringify({
|
|
342
|
+
type: 'output_omission',
|
|
343
|
+
line,
|
|
344
|
+
omittedMessages: state.omitted_messages,
|
|
345
|
+
omittedExportBytes: state.omitted_export_bytes,
|
|
346
|
+
sha256Chain: state.omission_digest,
|
|
347
|
+
retainedExportByteLimit: AGENT_OUTPUT_EXPORT_MAX_BYTES,
|
|
348
|
+
retainedMessageLimit: AGENT_OUTPUT_EXPORT_MAX_MESSAGES,
|
|
349
|
+
completeOutputAuthority: 'task_logs',
|
|
350
|
+
}),
|
|
351
|
+
metadata: JSON.stringify({
|
|
352
|
+
compactionReceipt: true,
|
|
353
|
+
contextSafe: false,
|
|
354
|
+
replayPolicy: 'raw_log_only',
|
|
355
|
+
}),
|
|
356
|
+
cluster_id: state.cluster_id,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
_persistAgentOutputReceipt(state) {
|
|
361
|
+
const receipt = this._agentOutputReceiptRecord(state);
|
|
362
|
+
const updated = state.receipt_message_id
|
|
363
|
+
? this.compactionStmts.updateReceipt.run(receipt.content_data, receipt.metadata, receipt.id)
|
|
364
|
+
: { changes: 0 };
|
|
365
|
+
if (updated.changes === 0) {
|
|
366
|
+
this._insertRecord(receipt);
|
|
367
|
+
}
|
|
368
|
+
state.receipt_message_id = receipt.id;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
_compactAgentOutput(clusterId) {
|
|
372
|
+
const state = this.compactionStmts.get.get(clusterId) || this._createCompactionState(clusterId);
|
|
373
|
+
while (
|
|
374
|
+
state.retained_export_bytes > AGENT_OUTPUT_EXPORT_MAX_BYTES ||
|
|
375
|
+
state.retained_messages > AGENT_OUTPUT_EXPORT_MAX_MESSAGES
|
|
376
|
+
) {
|
|
377
|
+
const oldest = this._oldestCompactableAgentOutput(clusterId);
|
|
378
|
+
if (!oldest) break;
|
|
379
|
+
const exportBytes = this._recordExportBytes(oldest);
|
|
380
|
+
state.first_omitted_timestamp ??= oldest.timestamp;
|
|
381
|
+
state.omitted_messages += 1;
|
|
382
|
+
state.omitted_export_bytes += exportBytes;
|
|
383
|
+
state.omission_digest = this._nextOmissionDigest(state.omission_digest, oldest);
|
|
384
|
+
state.retained_messages -= 1;
|
|
385
|
+
state.retained_export_bytes -= exportBytes;
|
|
386
|
+
if (
|
|
387
|
+
!state.receipt_message_id ||
|
|
388
|
+
!this.compactionStmts.receiptExists.get(state.receipt_message_id)
|
|
389
|
+
) {
|
|
390
|
+
this._persistAgentOutputReceipt(state);
|
|
391
|
+
}
|
|
392
|
+
this.compactionStmts.deleteMessage.run(oldest.id);
|
|
393
|
+
}
|
|
394
|
+
if (state.omitted_messages > 0) {
|
|
395
|
+
this._persistAgentOutputReceipt(state);
|
|
396
|
+
}
|
|
397
|
+
this._updateCompactionState(state);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
_insertAndCompact(record) {
|
|
401
|
+
const fullMessage = this._insertRecord(record);
|
|
402
|
+
if (this._isCompactableAgentOutput(record)) {
|
|
403
|
+
const state = this.compactionStmts.get.get(record.cluster_id);
|
|
404
|
+
if (state) {
|
|
405
|
+
state.retained_messages += 1;
|
|
406
|
+
state.retained_export_bytes += this._recordExportBytes(record);
|
|
407
|
+
this._updateCompactionState(state);
|
|
408
|
+
}
|
|
409
|
+
this._compactAgentOutput(record.cluster_id);
|
|
410
|
+
}
|
|
411
|
+
return fullMessage;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
_appendRecord(record) {
|
|
415
|
+
const compactable = this._isCompactableAgentOutput(record);
|
|
416
|
+
if (!this._appendRecordTxn) {
|
|
417
|
+
this._appendRecordTxn = this.db.transaction((item, compact) =>
|
|
418
|
+
compact ? this._insertAndCompact(item) : this._insertRecord(item)
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
return this._appendRecordTxn(record, compactable);
|
|
422
|
+
}
|
|
423
|
+
|
|
124
424
|
/**
|
|
125
425
|
* Append a message to the ledger
|
|
126
426
|
* @param {Object} message - Message object
|
|
@@ -154,18 +454,7 @@ class Ledger extends EventEmitter {
|
|
|
154
454
|
};
|
|
155
455
|
|
|
156
456
|
try {
|
|
157
|
-
const
|
|
158
|
-
record.id,
|
|
159
|
-
record.timestamp,
|
|
160
|
-
record.topic,
|
|
161
|
-
record.sender,
|
|
162
|
-
record.receiver,
|
|
163
|
-
record.content_text,
|
|
164
|
-
record.content_data,
|
|
165
|
-
record.metadata,
|
|
166
|
-
record.cluster_id
|
|
167
|
-
);
|
|
168
|
-
record.sequence = messageSequenceFromSql(insertResult.lastInsertRowid);
|
|
457
|
+
const fullMessage = this._appendRecord(record);
|
|
169
458
|
|
|
170
459
|
// Invalidate cache
|
|
171
460
|
this.cache.clear();
|
|
@@ -173,7 +462,6 @@ class Ledger extends EventEmitter {
|
|
|
173
462
|
this._lastTimestamp = Math.max(this._lastTimestamp, timestamp);
|
|
174
463
|
|
|
175
464
|
// Emit event for subscriptions
|
|
176
|
-
const fullMessage = this._deserializeMessage(record);
|
|
177
465
|
this.emit('message', fullMessage);
|
|
178
466
|
this.emit(`topic:${message.topic}`, fullMessage);
|
|
179
467
|
|
|
@@ -229,20 +517,7 @@ class Ledger extends EventEmitter {
|
|
|
229
517
|
cluster_id: message.cluster_id,
|
|
230
518
|
};
|
|
231
519
|
|
|
232
|
-
|
|
233
|
-
record.id,
|
|
234
|
-
record.timestamp,
|
|
235
|
-
record.topic,
|
|
236
|
-
record.sender,
|
|
237
|
-
record.receiver,
|
|
238
|
-
record.content_text,
|
|
239
|
-
record.content_data,
|
|
240
|
-
record.metadata,
|
|
241
|
-
record.cluster_id
|
|
242
|
-
);
|
|
243
|
-
record.sequence = messageSequenceFromSql(insertResult.lastInsertRowid);
|
|
244
|
-
|
|
245
|
-
results.push(this._deserializeMessage(record));
|
|
520
|
+
results.push(this._insertAndCompact(record));
|
|
246
521
|
}
|
|
247
522
|
|
|
248
523
|
return { results, baseTimestamp };
|
|
@@ -546,6 +821,54 @@ class Ledger extends EventEmitter {
|
|
|
546
821
|
return rows.map((row) => this._deserializeMessage(row));
|
|
547
822
|
}
|
|
548
823
|
|
|
824
|
+
/**
|
|
825
|
+
* Iterate over all cluster messages without materializing the complete ledger.
|
|
826
|
+
* @param {String} cluster_id - Cluster ID
|
|
827
|
+
* @yields {Object} One deserialized message at a time
|
|
828
|
+
*/
|
|
829
|
+
*iterateAll(cluster_id) {
|
|
830
|
+
for (const row of this.stmts.getAll.iterate(cluster_id)) {
|
|
831
|
+
yield this._deserializeMessage(row);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Run synchronous reads against one stable SQLite snapshot.
|
|
837
|
+
* @param {Function} callback - Work to perform inside the snapshot
|
|
838
|
+
* @returns {*} The callback result
|
|
839
|
+
*/
|
|
840
|
+
withReadSnapshot(callback) {
|
|
841
|
+
return this.db.transaction(callback).deferred();
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Return whether a legacy cluster requires one writable compaction pass.
|
|
846
|
+
* This preflight is read-only and does not acquire a writer lock.
|
|
847
|
+
* @param {String} cluster_id - Cluster ID
|
|
848
|
+
* @returns {Boolean} True when actual raw output disagrees with durable budget state
|
|
849
|
+
*/
|
|
850
|
+
needsAgentOutputReconciliation(cluster_id) {
|
|
851
|
+
const hasCompactionTable = this.db
|
|
852
|
+
.prepare(
|
|
853
|
+
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'agent_output_compaction'"
|
|
854
|
+
)
|
|
855
|
+
.get();
|
|
856
|
+
const state =
|
|
857
|
+
hasCompactionTable &&
|
|
858
|
+
this.db.prepare('SELECT * FROM agent_output_compaction WHERE cluster_id = ?').get(cluster_id);
|
|
859
|
+
const measured = this._measureCompactableAgentOutput(cluster_id);
|
|
860
|
+
if (!state) return measured.retained_messages > 0;
|
|
861
|
+
if (
|
|
862
|
+
state.retained_messages !== measured.retained_messages ||
|
|
863
|
+
state.retained_export_bytes !== measured.retained_export_bytes
|
|
864
|
+
) {
|
|
865
|
+
return true;
|
|
866
|
+
}
|
|
867
|
+
if (state.omitted_messages === 0) return false;
|
|
868
|
+
if (!state.receipt_message_id) return true;
|
|
869
|
+
return !this.db.prepare('SELECT 1 FROM messages WHERE id = ?').get(state.receipt_message_id);
|
|
870
|
+
}
|
|
871
|
+
|
|
549
872
|
/**
|
|
550
873
|
* Get aggregated token usage by agent role
|
|
551
874
|
* Queries TOKEN_USAGE messages and sums tokens per role
|
|
@@ -811,9 +1134,19 @@ class Ledger extends EventEmitter {
|
|
|
811
1134
|
* Clear all messages (for testing)
|
|
812
1135
|
*/
|
|
813
1136
|
clear() {
|
|
814
|
-
this.db.
|
|
1137
|
+
this.db.transaction(() => {
|
|
1138
|
+
this.db.exec('DELETE FROM messages');
|
|
1139
|
+
if (this.compactionStmts) {
|
|
1140
|
+
this.compactionStmts.deleteState.run();
|
|
1141
|
+
}
|
|
1142
|
+
})();
|
|
815
1143
|
this.cache.clear();
|
|
816
1144
|
}
|
|
817
1145
|
}
|
|
818
1146
|
|
|
1147
|
+
Ledger.AGENT_OUTPUT_EXPORT_LIMITS = Object.freeze({
|
|
1148
|
+
maxBytes: AGENT_OUTPUT_EXPORT_MAX_BYTES,
|
|
1149
|
+
maxMessages: AGENT_OUTPUT_EXPORT_MAX_MESSAGES,
|
|
1150
|
+
});
|
|
1151
|
+
|
|
819
1152
|
module.exports = Ledger;
|
package/src/template-resolver.js
CHANGED
|
@@ -327,6 +327,11 @@ class TemplateResolver {
|
|
|
327
327
|
* @returns {any}
|
|
328
328
|
*/
|
|
329
329
|
_resolveString(str, params) {
|
|
330
|
+
const exactPlaceholder = str.match(/^\{\{(\w+)\}\}$/);
|
|
331
|
+
if (exactPlaceholder && params[exactPlaceholder[1]] !== undefined) {
|
|
332
|
+
return JSON.parse(JSON.stringify(params[exactPlaceholder[1]]));
|
|
333
|
+
}
|
|
334
|
+
|
|
330
335
|
// Handle simple {{param}} substitutions
|
|
331
336
|
let result = str.replace(
|
|
332
337
|
/\{\{(\w+)\}\}/g,
|