@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/src/ledger.js CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Ledger - Immutable event log for multi-agent coordination
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 (id, timestamp, topic, sender, receiver, content_text, content_data, metadata, cluster_id)
99
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
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 insertResult = this.stmts.insert.run(
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
- const insertResult = this.stmts.insert.run(
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.exec('DELETE FROM messages');
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;
@@ -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,