@the-open-engine/zeroshot 6.34.1 → 6.34.3

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;
@@ -275,6 +275,7 @@ class Orchestrator {
275
275
  // Track if orchestrator is closed (prevents _saveClusters race conditions during cleanup)
276
276
  this.closed = false;
277
277
  this._conductorWatchdogs = new Set();
278
+ this._clusterRunBoundaries = new Map();
278
279
 
279
280
  // Track if clusters are loaded (for lazy loading pattern)
280
281
  this._clustersLoaded = options.skipLoad === true;
@@ -1575,11 +1576,17 @@ class Orchestrator {
1575
1576
  }
1576
1577
 
1577
1578
  _registerAgentErrorHandler(messageBus, clusterId) {
1579
+ this._recordClusterRunBoundary(messageBus, clusterId);
1578
1580
  this._subscribeToClusterTopic(messageBus, clusterId, 'AGENT_ERROR', async (message) => {
1579
1581
  const agentRole = message.content?.data?.role;
1580
1582
  const attempts = message.content?.data?.attempts || 1;
1581
1583
  const hookFailure = message.content?.data?.hookFailure === true;
1582
1584
  const restartExhausted = message.content?.data?.restartExhausted === true;
1585
+ const durableClusterFailure = this._findCurrentRunClusterFailure(
1586
+ messageBus,
1587
+ clusterId,
1588
+ message.sequence
1589
+ );
1583
1590
 
1584
1591
  await this._saveClusters();
1585
1592
 
@@ -1587,7 +1594,10 @@ class Orchestrator {
1587
1594
  agentRole === 'implementation' ||
1588
1595
  agentRole === 'coordinator' ||
1589
1596
  message.sender === 'consensus-coordinator';
1590
- const shouldStop = shouldStopForRole && (hookFailure || restartExhausted || attempts >= 3);
1597
+ const shouldStop =
1598
+ !durableClusterFailure &&
1599
+ shouldStopForRole &&
1600
+ (hookFailure || restartExhausted || attempts >= 3);
1591
1601
 
1592
1602
  if (shouldStop) {
1593
1603
  this._log(`\n${'='.repeat(80)}`);
@@ -1610,6 +1620,25 @@ class Orchestrator {
1610
1620
  });
1611
1621
  }
1612
1622
 
1623
+ _recordClusterRunBoundary(messageBus, clusterId) {
1624
+ const latest = messageBus.findLast({ cluster_id: clusterId, orderBySequence: true });
1625
+ this._clusterRunBoundaries.set(clusterId, latest?.sequence ?? null);
1626
+ }
1627
+
1628
+ _findCurrentRunClusterFailure(messageBus, clusterId, throughId) {
1629
+ const boundary = this._clusterRunBoundaries.get(clusterId);
1630
+ return (
1631
+ messageBus.query({
1632
+ cluster_id: clusterId,
1633
+ topic: 'CLUSTER_FAILED',
1634
+ order: 'desc',
1635
+ limit: 1,
1636
+ ...(boundary === null ? {} : { afterId: boundary }),
1637
+ ...(throughId === undefined ? {} : { throughId }),
1638
+ })[0] || null
1639
+ );
1640
+ }
1641
+
1613
1642
  _registerPushBlockedHandler(messageBus, clusterId) {
1614
1643
  this._subscribeToClusterTopic(messageBus, clusterId, 'PUSH_BLOCKED', async (message) => {
1615
1644
  const reason = message.content?.data?.blocked_reason || 'unknown';
@@ -2425,6 +2454,7 @@ class Orchestrator {
2425
2454
 
2426
2455
  // Now remove from memory after persisting
2427
2456
  this.clusters.delete(clusterId);
2457
+ this._clusterRunBoundaries.delete(clusterId);
2428
2458
  }
2429
2459
 
2430
2460
  /**
@@ -2462,6 +2492,7 @@ class Orchestrator {
2462
2492
  watchdog.dispose();
2463
2493
  }
2464
2494
  this._conductorWatchdogs.clear();
2495
+ this._clusterRunBoundaries.clear();
2465
2496
 
2466
2497
  for (const cluster of this.clusters.values()) {
2467
2498
  if (typeof cluster.snapshotter?.stop === 'function') {
@@ -2902,6 +2933,7 @@ class Orchestrator {
2902
2933
  }
2903
2934
 
2904
2935
  async _restartClusterAgents(cluster) {
2936
+ this._recordClusterRunBoundary(cluster.messageBus, cluster.id);
2905
2937
  cluster.state = 'running';
2906
2938
  cluster.pid = process.pid;
2907
2939
  for (const agent of cluster.agents) {
@@ -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,