@covibes/zeroshot 1.3.0 → 1.5.0

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.
@@ -86,6 +86,7 @@ class StatusFooter {
86
86
  this.clusterId = null;
87
87
  this.clusterState = 'initializing';
88
88
  this.startTime = Date.now();
89
+ this.messageBus = null; // MessageBus for token usage tracking
89
90
 
90
91
  // Robust resize handling state
91
92
  this.isRendering = false; // Render lock - prevents concurrent renders
@@ -95,6 +96,11 @@ class StatusFooter {
95
96
  this.minRows = 8; // Minimum rows for footer display (graceful degradation)
96
97
  this.hidden = false; // True when terminal too small for footer
97
98
 
99
+ // Output queue - serializes all stdout to prevent cursor corruption
100
+ // When scroll region is active, console.log() can corrupt cursor position
101
+ // All output must go through print() to coordinate with render cycles
102
+ this.printQueue = [];
103
+
98
104
  // Debounced resize handler (100ms) - prevents rapid-fire redraws
99
105
  this._debouncedResize = debounce(() => this._handleResize(), 100);
100
106
  }
@@ -107,6 +113,38 @@ class StatusFooter {
107
113
  return process.stdout.isTTY === true;
108
114
  }
109
115
 
116
+ /**
117
+ * Print text to stdout, coordinating with the render cycle.
118
+ * When a render is in progress, queues output to prevent cursor corruption.
119
+ * When no render is active, writes immediately.
120
+ *
121
+ * MUST be used instead of console.log() when status footer is active.
122
+ * @param {string} text - Text to print (newline will be added)
123
+ */
124
+ print(text) {
125
+ if (this.isRendering) {
126
+ // Queue for later - render() will flush after restoring cursor
127
+ this.printQueue.push(text);
128
+ } else {
129
+ // Write immediately - no render in progress
130
+ process.stdout.write(text + '\n');
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Flush queued output to stdout.
136
+ * Called after render() restores cursor to ensure proper positioning.
137
+ * @private
138
+ */
139
+ _flushPrintQueue() {
140
+ if (this.printQueue.length === 0) return;
141
+
142
+ // Write all queued output
143
+ const output = this.printQueue.map(text => text + '\n').join('');
144
+ this.printQueue = [];
145
+ process.stdout.write(output);
146
+ }
147
+
110
148
  /**
111
149
  * Get terminal dimensions
112
150
  * @returns {{ rows: number, cols: number }}
@@ -137,23 +175,60 @@ class StatusFooter {
137
175
  }
138
176
 
139
177
  /**
140
- * Clear all footer lines (uses last known height for safety)
178
+ * Generate move cursor ANSI sequence (returns string, doesn't write)
179
+ * Used for atomic buffered writes to prevent interleaving
180
+ * @param {number} row - 1-based row
181
+ * @param {number} col - 1-based column
182
+ * @returns {string} ANSI escape sequence
141
183
  * @private
142
184
  */
143
- _clearFooterArea() {
185
+ _moveToStr(row, col) {
186
+ return `${CSI}${row};${col}H`;
187
+ }
188
+
189
+ /**
190
+ * Generate clear line ANSI sequence (returns string, doesn't write)
191
+ * Used for atomic buffered writes to prevent interleaving
192
+ * @param {number} row - 1-based row number
193
+ * @returns {string} ANSI escape sequence
194
+ * @private
195
+ */
196
+ _clearLineStr(row) {
197
+ return `${CSI}${row};1H${CLEAR_LINE}`;
198
+ }
199
+
200
+ /**
201
+ * Generate ANSI sequences to clear all footer lines (returns string)
202
+ * Used for atomic buffered writes to prevent interleaving
203
+ * @returns {string} ANSI escape sequences
204
+ * @private
205
+ */
206
+ _clearFooterAreaStr() {
144
207
  const { rows } = this.getTerminalSize();
145
208
  // Use max of current and last footer height to ensure full cleanup
146
209
  const heightToClear = Math.max(this.footerHeight, this.lastFooterHeight, 3);
147
210
  const startRow = Math.max(1, rows - heightToClear + 1);
148
211
 
212
+ let buffer = '';
149
213
  for (let row = startRow; row <= rows; row++) {
150
- this._clearLine(row);
214
+ buffer += this._clearLineStr(row);
151
215
  }
216
+ return buffer;
217
+ }
218
+
219
+ /**
220
+ * Clear all footer lines (uses last known height for safety)
221
+ * Uses single atomic write to prevent interleaving with other processes
222
+ * @private
223
+ */
224
+ _clearFooterArea() {
225
+ process.stdout.write(this._clearFooterAreaStr());
152
226
  }
153
227
 
154
228
  /**
155
229
  * Set up scroll region to reserve space for footer
156
230
  * ROBUST: Clears footer area first, resets to full screen, then sets new region
231
+ * Uses single atomic write to prevent interleaving with other processes
157
232
  */
158
233
  setupScrollRegion() {
159
234
  if (!this.isTTY()) return;
@@ -178,39 +253,53 @@ class StatusFooter {
178
253
 
179
254
  const scrollEnd = rows - this.footerHeight;
180
255
 
181
- // CRITICAL: Save cursor before any manipulation
182
- process.stdout.write(SAVE_CURSOR);
183
- process.stdout.write(HIDE_CURSOR);
256
+ // BUILD ENTIRE OUTPUT INTO SINGLE BUFFER for atomic write
257
+ let buffer = '';
258
+
259
+ // Step 1: Save cursor before any manipulation
260
+ buffer += SAVE_CURSOR;
261
+ buffer += HIDE_CURSOR;
184
262
 
185
- // Step 1: Reset scroll region to full screen first (prevents artifacts)
186
- process.stdout.write(`${CSI}1;${rows}r`);
263
+ // Step 2: Reset scroll region to full screen first (prevents artifacts)
264
+ buffer += `${CSI}1;${rows}r`;
187
265
 
188
- // Step 2: Clear footer area completely (prevents ghosting)
189
- this._clearFooterArea();
266
+ // Step 3: Clear footer area completely (prevents ghosting)
267
+ buffer += this._clearFooterAreaStr();
190
268
 
191
- // Step 3: Set new scroll region (lines 1 to scrollEnd)
192
- process.stdout.write(`${CSI}1;${scrollEnd}r`);
269
+ // Step 4: Set new scroll region (lines 1 to scrollEnd)
270
+ buffer += `${CSI}1;${scrollEnd}r`;
193
271
 
194
- // Step 4: Move cursor to bottom of scroll region (safe position)
195
- process.stdout.write(`${CSI}${scrollEnd};1H`);
272
+ // Step 5: Move cursor to bottom of scroll region (safe position)
273
+ buffer += this._moveToStr(scrollEnd, 1);
196
274
 
197
- // Restore cursor and show it
198
- process.stdout.write(RESTORE_CURSOR);
199
- process.stdout.write(SHOW_CURSOR);
275
+ // Step 6: Restore cursor and show it
276
+ buffer += RESTORE_CURSOR;
277
+ buffer += SHOW_CURSOR;
278
+
279
+ // SINGLE ATOMIC WRITE - prevents interleaving
280
+ process.stdout.write(buffer);
200
281
 
201
282
  this.scrollRegionSet = true;
202
283
  this.lastKnownRows = rows;
203
284
  this.lastKnownCols = cols;
204
285
  }
205
286
 
287
+ /**
288
+ * Generate reset scroll region string (returns string, doesn't write)
289
+ * @private
290
+ */
291
+ _resetScrollRegionStr() {
292
+ const { rows } = this.getTerminalSize();
293
+ return `${CSI}1;${rows}r`;
294
+ }
295
+
206
296
  /**
207
297
  * Reset scroll region to full terminal
208
298
  */
209
299
  resetScrollRegion() {
210
300
  if (!this.isTTY()) return;
211
301
 
212
- const { rows } = this.getTerminalSize();
213
- process.stdout.write(`${CSI}1;${rows}r`);
302
+ process.stdout.write(this._resetScrollRegionStr());
214
303
  this.scrollRegionSet = false;
215
304
  }
216
305
 
@@ -251,6 +340,14 @@ class StatusFooter {
251
340
  this.clusterId = clusterId;
252
341
  }
253
342
 
343
+ /**
344
+ * Set message bus for token usage tracking
345
+ * @param {object} messageBus - MessageBus instance with getTokensByRole()
346
+ */
347
+ setMessageBus(messageBus) {
348
+ this.messageBus = messageBus;
349
+ }
350
+
254
351
  /**
255
352
  * Update cluster state
256
353
  * @param {string} state
@@ -401,35 +498,53 @@ class StatusFooter {
401
498
  const agentRows = this.buildAgentRows(executingAgents, cols);
402
499
  const summaryLine = this.buildSummaryLine(cols);
403
500
 
404
- // Save cursor, render footer, restore cursor
405
- process.stdout.write(SAVE_CURSOR);
406
- process.stdout.write(HIDE_CURSOR);
501
+ // BUILD ENTIRE OUTPUT INTO SINGLE BUFFER for atomic write
502
+ // This prevents interleaving with other processes writing to stdout
503
+ let buffer = '';
504
+ buffer += SAVE_CURSOR;
505
+ buffer += HIDE_CURSOR;
407
506
 
408
507
  // Render from top of footer area
409
508
  let currentRow = rows - this.footerHeight + 1;
410
509
 
411
510
  // Header line
412
- this.moveTo(currentRow++, 1);
413
- process.stdout.write(CLEAR_LINE);
414
- process.stdout.write(`${COLORS.bgBlack}${headerLine}${COLORS.reset}`);
511
+ buffer += this._moveToStr(currentRow++, 1);
512
+ buffer += CLEAR_LINE;
513
+ buffer += `${COLORS.bgBlack}${headerLine}${COLORS.reset}`;
415
514
 
416
515
  // Agent rows
417
516
  for (const agentRow of agentRows) {
418
- this.moveTo(currentRow++, 1);
419
- process.stdout.write(CLEAR_LINE);
420
- process.stdout.write(`${COLORS.bgBlack}${agentRow}${COLORS.reset}`);
517
+ buffer += this._moveToStr(currentRow++, 1);
518
+ buffer += CLEAR_LINE;
519
+ buffer += `${COLORS.bgBlack}${agentRow}${COLORS.reset}`;
421
520
  }
422
521
 
423
522
  // Summary line (with bottom border)
424
- this.moveTo(currentRow, 1);
425
- process.stdout.write(CLEAR_LINE);
426
- process.stdout.write(`${COLORS.bgBlack}${summaryLine}${COLORS.reset}`);
523
+ buffer += this._moveToStr(currentRow, 1);
524
+ buffer += CLEAR_LINE;
525
+ buffer += `${COLORS.bgBlack}${summaryLine}${COLORS.reset}`;
526
+
527
+ buffer += RESTORE_CURSOR;
528
+ buffer += SHOW_CURSOR;
427
529
 
428
- process.stdout.write(RESTORE_CURSOR);
429
- process.stdout.write(SHOW_CURSOR);
530
+ // SINGLE ATOMIC WRITE - prevents interleaving
531
+ process.stdout.write(buffer);
430
532
  } finally {
431
533
  this.isRendering = false;
432
534
 
535
+ // CRITICAL: Position cursor at bottom of scroll region before flushing
536
+ // Without this, output goes below footer if cursor was restored outside scroll region
537
+ // (RESTORE_CURSOR at line 527 may restore to row outside the scrollable area)
538
+ if (this.scrollRegionSet) {
539
+ const { rows } = this.getTerminalSize();
540
+ const scrollEnd = rows - this.footerHeight;
541
+ process.stdout.write(this._moveToStr(scrollEnd, 1));
542
+ }
543
+
544
+ // Flush any output that was queued during render
545
+ // Must happen BEFORE pending resize to preserve output order
546
+ this._flushPrintQueue();
547
+
433
548
  // Process pending resize if one was queued during render
434
549
  if (this.pendingResize) {
435
550
  this.pendingResize = false;
@@ -554,6 +669,21 @@ class StatusFooter {
554
669
  const total = this.agents.size;
555
670
  parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.green}${executing}/${total}${COLORS.reset} active`);
556
671
 
672
+ // Token cost (from message bus)
673
+ if (this.messageBus && this.clusterId) {
674
+ try {
675
+ const tokensByRole = this.messageBus.getTokensByRole(this.clusterId);
676
+ const totalCost = tokensByRole?._total?.totalCostUsd || 0;
677
+ if (totalCost > 0) {
678
+ // Format: $0.05 or $1.23 or $12.34
679
+ const costStr = totalCost < 0.01 ? '<$0.01' : `$${totalCost.toFixed(2)}`;
680
+ parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.yellow}${costStr}${COLORS.reset}`);
681
+ }
682
+ } catch {
683
+ // Ignore errors - token tracking is optional
684
+ }
685
+ }
686
+
557
687
  // Aggregate metrics
558
688
  let totalCpu = 0;
559
689
  let totalMem = 0;
@@ -621,7 +751,9 @@ class StatusFooter {
621
751
  process.stdout.on('resize', this._debouncedResize);
622
752
 
623
753
  // Start refresh interval
754
+ // Guard: Skip if previous render still running (prevents overlapping renders)
624
755
  this.intervalId = setInterval(() => {
756
+ if (this.isRendering) return;
625
757
  this.render();
626
758
  }, this.refreshInterval);
627
759
 
@@ -642,17 +774,27 @@ class StatusFooter {
642
774
  process.stdout.removeListener('resize', this._debouncedResize);
643
775
 
644
776
  if (this.isTTY() && !this.hidden) {
645
- // Reset scroll region
646
- this.resetScrollRegion();
777
+ // BUILD SINGLE BUFFER for atomic shutdown write
778
+ // Prevents interleaving with agent output during cleanup
779
+ let buffer = '';
647
780
 
648
- // Clear all footer lines
649
- this._clearFooterArea();
781
+ // CRITICAL: Clear footer area BEFORE resetting scroll region
782
+ // While scroll region is active, footer area contains only status bar content
783
+ // After reset, those lines may contain scrolled output (which we DON'T want to clear)
784
+ buffer += this._clearFooterAreaStr();
650
785
 
651
- // Move cursor to safe position
786
+ // Now reset scroll region (full terminal is scrollable again)
787
+ buffer += this._resetScrollRegionStr();
788
+ this.scrollRegionSet = false;
789
+
790
+ // Move cursor to safe position and show cursor
652
791
  const { rows } = this.getTerminalSize();
653
792
  const startRow = rows - this.footerHeight + 1;
654
- this.moveTo(startRow, 1);
655
- process.stdout.write(SHOW_CURSOR);
793
+ buffer += this._moveToStr(startRow, 1);
794
+ buffer += SHOW_CURSOR;
795
+
796
+ // SINGLE ATOMIC WRITE
797
+ process.stdout.write(buffer);
656
798
  }
657
799
  }
658
800
 
@@ -662,8 +804,12 @@ class StatusFooter {
662
804
  hide() {
663
805
  if (!this.isTTY()) return;
664
806
 
665
- this.resetScrollRegion();
666
- this._clearFooterArea();
807
+ // Single atomic write for hide operation
808
+ // CRITICAL: Clear footer BEFORE resetting scroll region (same reason as stop())
809
+ let buffer = this._clearFooterAreaStr();
810
+ buffer += this._resetScrollRegionStr();
811
+ this.scrollRegionSet = false;
812
+ process.stdout.write(buffer);
667
813
  }
668
814
 
669
815
  /**
@@ -43,8 +43,11 @@ class TemplateResolver {
43
43
  // Validate required params
44
44
  this._validateParams(template, params);
45
45
 
46
+ // Apply defaults for missing params (e.g., timeout: 0)
47
+ const paramsWithDefaults = this._applyDefaults(template, params);
48
+
46
49
  // Deep clone and resolve
47
- const resolved = this._resolveObject(JSON.parse(JSON.stringify(template)), params);
50
+ const resolved = this._resolveObject(JSON.parse(JSON.stringify(template)), paramsWithDefaults);
48
51
 
49
52
  // Filter out conditional agents that don't meet their condition
50
53
  if (resolved.agents) {
@@ -86,6 +89,25 @@ class TemplateResolver {
86
89
  }
87
90
  }
88
91
 
92
+ /**
93
+ * Apply template defaults for any missing params
94
+ * @private
95
+ * @param {any} template
96
+ * @param {any} params
97
+ * @returns {any} params with defaults applied
98
+ */
99
+ _applyDefaults(template, params) {
100
+ if (!template.params) return params;
101
+
102
+ const result = { ...params };
103
+ for (const [name, schema] of Object.entries(template.params)) {
104
+ if (result[name] === undefined && schema.default !== undefined) {
105
+ result[name] = schema.default;
106
+ }
107
+ }
108
+ return result;
109
+ }
110
+
89
111
  /**
90
112
  * Recursively resolve placeholders in an object
91
113
  * @private