@covibes/zeroshot 5.3.0 → 5.4.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.
- package/README.md +94 -14
- package/cli/commands/providers.js +8 -9
- package/cli/index.js +3032 -2409
- package/cli/message-formatters-normal.js +28 -6
- package/cluster-templates/base-templates/debug-workflow.json +1 -1
- package/cluster-templates/base-templates/full-workflow.json +72 -188
- package/cluster-templates/base-templates/worker-validator.json +59 -3
- package/cluster-templates/conductor-bootstrap.json +4 -4
- package/lib/docker-config.js +8 -0
- package/lib/git-remote-utils.js +165 -0
- package/lib/id-detector.js +10 -7
- package/lib/provider-defaults.js +62 -0
- package/lib/provider-names.js +2 -1
- package/lib/settings/claude-auth.js +78 -0
- package/lib/settings.js +161 -63
- package/package.json +7 -2
- package/scripts/setup-merge-queue.sh +170 -0
- package/src/agent/agent-config.js +135 -82
- package/src/agent/agent-context-builder.js +297 -188
- package/src/agent/agent-hook-executor.js +310 -113
- package/src/agent/agent-lifecycle.js +385 -325
- package/src/agent/agent-stuck-detector.js +7 -7
- package/src/agent/agent-task-executor.js +824 -565
- package/src/agent/output-extraction.js +41 -24
- package/src/agent/output-reformatter.js +1 -1
- package/src/agent/schema-utils.js +108 -73
- package/src/agent-wrapper.js +10 -1
- package/src/agents/git-pusher-template.js +285 -0
- package/src/claude-task-runner.js +85 -34
- package/src/config-validator.js +922 -657
- package/src/input-helpers.js +65 -0
- package/src/isolation-manager.js +289 -199
- package/src/issue-providers/README.md +305 -0
- package/src/issue-providers/azure-devops-provider.js +273 -0
- package/src/issue-providers/base-provider.js +232 -0
- package/src/issue-providers/github-provider.js +179 -0
- package/src/issue-providers/gitlab-provider.js +241 -0
- package/src/issue-providers/index.js +196 -0
- package/src/issue-providers/jira-provider.js +239 -0
- package/src/ledger.js +22 -5
- package/src/lib/safe-exec.js +88 -0
- package/src/orchestrator.js +1107 -811
- package/src/preflight.js +313 -159
- package/src/process-metrics.js +98 -56
- package/src/providers/anthropic/cli-builder.js +53 -25
- package/src/providers/anthropic/index.js +72 -2
- package/src/providers/anthropic/output-parser.js +32 -14
- package/src/providers/base-provider.js +70 -0
- package/src/providers/capabilities.js +9 -0
- package/src/providers/google/output-parser.js +47 -38
- package/src/providers/index.js +19 -3
- package/src/providers/openai/cli-builder.js +14 -3
- package/src/providers/openai/index.js +1 -0
- package/src/providers/openai/output-parser.js +44 -30
- package/src/providers/opencode/cli-builder.js +42 -0
- package/src/providers/opencode/index.js +103 -0
- package/src/providers/opencode/models.js +55 -0
- package/src/providers/opencode/output-parser.js +122 -0
- package/src/schemas/sub-cluster.js +68 -39
- package/src/status-footer.js +94 -48
- package/src/sub-cluster-wrapper.js +76 -35
- package/src/template-resolver.js +12 -9
- package/src/tui/data-poller.js +123 -99
- package/src/tui/formatters.js +4 -3
- package/src/tui/keybindings.js +259 -318
- package/src/tui/renderer.js +11 -21
- package/task-lib/attachable-watcher.js +118 -81
- package/task-lib/claude-recovery.js +61 -24
- package/task-lib/commands/episodes.js +105 -0
- package/task-lib/commands/list.js +2 -2
- package/task-lib/commands/logs.js +231 -189
- package/task-lib/commands/schedules.js +111 -61
- package/task-lib/config.js +0 -2
- package/task-lib/runner.js +84 -27
- package/task-lib/scheduler.js +1 -1
- package/task-lib/store.js +467 -168
- package/task-lib/tui/formatters.js +94 -45
- package/task-lib/tui/renderer.js +13 -3
- package/task-lib/tui.js +73 -32
- package/task-lib/watcher.js +128 -90
- package/src/agents/git-pusher-agent.json +0 -20
- package/src/github.js +0 -139
package/src/status-footer.js
CHANGED
|
@@ -58,7 +58,7 @@ const AGENT_STATE = Object.freeze({
|
|
|
58
58
|
// Agent states that indicate "active" work (from agent-lifecycle.js)
|
|
59
59
|
// These should show in the footer with metrics
|
|
60
60
|
const ACTIVE_STATES = new Set([
|
|
61
|
-
AGENT_STATE.EXECUTING_TASK,
|
|
61
|
+
AGENT_STATE.EXECUTING_TASK, // Actually running claude CLI
|
|
62
62
|
AGENT_STATE.EVALUATING_LOGIC, // Evaluating trigger conditions
|
|
63
63
|
AGENT_STATE.BUILDING_CONTEXT, // Building context for task
|
|
64
64
|
]);
|
|
@@ -81,7 +81,8 @@ function debounce(fn, ms) {
|
|
|
81
81
|
* @typedef {Object} AgentState
|
|
82
82
|
* @property {string} id - Agent ID
|
|
83
83
|
* @property {string} state - Agent state (idle, executing, etc.)
|
|
84
|
-
* @property {number|null}
|
|
84
|
+
* @property {number|null} processPid - Process ID if running (preferred)
|
|
85
|
+
* @property {number|null} pid - Legacy alias for processPid
|
|
85
86
|
* @property {number} iteration - Current iteration
|
|
86
87
|
*/
|
|
87
88
|
|
|
@@ -175,7 +176,7 @@ class StatusFooter {
|
|
|
175
176
|
if (this.printQueue.length === 0) return;
|
|
176
177
|
|
|
177
178
|
// Write all queued output
|
|
178
|
-
const output = this.printQueue.map(text => text + '\n').join('');
|
|
179
|
+
const output = this.printQueue.map((text) => text + '\n').join('');
|
|
179
180
|
this.printQueue = [];
|
|
180
181
|
process.stdout.write(output);
|
|
181
182
|
}
|
|
@@ -396,8 +397,11 @@ class StatusFooter {
|
|
|
396
397
|
* @param {AgentState} agentState
|
|
397
398
|
*/
|
|
398
399
|
updateAgent(agentState) {
|
|
400
|
+
const processPid = agentState.processPid ?? agentState.pid ?? null;
|
|
399
401
|
this.agents.set(agentState.id, {
|
|
400
402
|
...agentState,
|
|
403
|
+
processPid,
|
|
404
|
+
pid: processPid,
|
|
401
405
|
lastUpdate: Date.now(),
|
|
402
406
|
});
|
|
403
407
|
}
|
|
@@ -418,11 +422,12 @@ class StatusFooter {
|
|
|
418
422
|
*/
|
|
419
423
|
async _sampleMetrics() {
|
|
420
424
|
for (const [agentId, agent] of this.agents) {
|
|
421
|
-
|
|
425
|
+
const processPid = agent.processPid ?? agent.pid;
|
|
426
|
+
if (!processPid) continue;
|
|
422
427
|
|
|
423
428
|
try {
|
|
424
429
|
// Get actual metrics with 200ms sample window (for CPU calculation)
|
|
425
|
-
const raw = await getProcessMetrics(
|
|
430
|
+
const raw = await getProcessMetrics(processPid, { samplePeriodMs: 200 });
|
|
426
431
|
const existing = this.interpolatedMetrics.get(agentId);
|
|
427
432
|
|
|
428
433
|
this.interpolatedMetrics.set(agentId, {
|
|
@@ -458,14 +463,8 @@ class StatusFooter {
|
|
|
458
463
|
if (!data.target || !data.current) continue;
|
|
459
464
|
|
|
460
465
|
// Lerp CPU and RAM toward target (they fluctuate)
|
|
461
|
-
data.current.cpuPercent = this._lerp(
|
|
462
|
-
|
|
463
|
-
data.target.cpuPercent
|
|
464
|
-
);
|
|
465
|
-
data.current.memoryMB = this._lerp(
|
|
466
|
-
data.current.memoryMB,
|
|
467
|
-
data.target.memoryMB
|
|
468
|
-
);
|
|
466
|
+
data.current.cpuPercent = this._lerp(data.current.cpuPercent, data.target.cpuPercent);
|
|
467
|
+
data.current.memoryMB = this._lerp(data.current.memoryMB, data.target.memoryMB);
|
|
469
468
|
// Network is cumulative counter - no interpolation, just use latest
|
|
470
469
|
}
|
|
471
470
|
}
|
|
@@ -767,60 +766,107 @@ class StatusFooter {
|
|
|
767
766
|
// Border with corner
|
|
768
767
|
parts.push(`${COLORS.gray}└─${COLORS.reset}`);
|
|
769
768
|
|
|
770
|
-
|
|
769
|
+
this.appendClusterStateSummary(parts);
|
|
770
|
+
this.appendDurationSummary(parts);
|
|
771
|
+
this.appendAgentCountSummary(parts);
|
|
772
|
+
this.appendTokenCostSummary(parts);
|
|
773
|
+
this.appendAggregateMetricsSummary(parts);
|
|
774
|
+
|
|
775
|
+
// Pad and close with bottom corner
|
|
776
|
+
return this.padSummaryLine(parts, width);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
appendClusterStateSummary(parts) {
|
|
771
780
|
const stateColor = this.clusterState === 'running' ? COLORS.green : COLORS.yellow;
|
|
772
781
|
parts.push(` ${stateColor}${this.clusterState}${COLORS.reset}`);
|
|
782
|
+
}
|
|
773
783
|
|
|
774
|
-
|
|
784
|
+
appendDurationSummary(parts) {
|
|
775
785
|
const duration = this.formatDuration(Date.now() - this.startTime);
|
|
776
786
|
parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.dim}${duration}${COLORS.reset}`);
|
|
787
|
+
}
|
|
777
788
|
|
|
778
|
-
|
|
779
|
-
const executing =
|
|
780
|
-
|
|
781
|
-
|
|
789
|
+
appendAgentCountSummary(parts) {
|
|
790
|
+
const { executing, total } = this.getActiveAgentCounts();
|
|
791
|
+
parts.push(
|
|
792
|
+
` ${COLORS.gray}│${COLORS.reset} ${COLORS.green}${executing}/${total}${COLORS.reset} active`
|
|
793
|
+
);
|
|
794
|
+
}
|
|
782
795
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
796
|
+
getActiveAgentCounts() {
|
|
797
|
+
const executing = Array.from(this.agents.values()).filter((a) =>
|
|
798
|
+
ACTIVE_STATES.has(a.state)
|
|
799
|
+
).length;
|
|
800
|
+
return { executing, total: this.agents.size };
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
appendTokenCostSummary(parts) {
|
|
804
|
+
const costStr = this.getTokenCostSummary();
|
|
805
|
+
if (!costStr) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.yellow}${costStr}${COLORS.reset}`);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
getTokenCostSummary() {
|
|
812
|
+
if (!this.messageBus || !this.clusterId) {
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
try {
|
|
817
|
+
const tokensByRole = this.messageBus.getTokensByRole(this.clusterId);
|
|
818
|
+
const totalCost = tokensByRole?._total?.totalCostUsd || 0;
|
|
819
|
+
if (totalCost <= 0) {
|
|
820
|
+
return null;
|
|
795
821
|
}
|
|
822
|
+
|
|
823
|
+
// Format: $0.05 or $1.23 or $12.34
|
|
824
|
+
return totalCost < 0.01 ? '<$0.01' : `$${totalCost.toFixed(2)}`;
|
|
825
|
+
} catch {
|
|
826
|
+
// Ignore errors - token tracking is optional
|
|
827
|
+
return null;
|
|
796
828
|
}
|
|
829
|
+
}
|
|
797
830
|
|
|
798
|
-
|
|
831
|
+
appendAggregateMetricsSummary(parts) {
|
|
832
|
+
const { totalCpu, totalMem, totalBytesSent, totalBytesReceived } = this.getAggregateMetrics();
|
|
833
|
+
|
|
834
|
+
if (totalCpu <= 0 && totalMem <= 0) {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
parts.push(` ${COLORS.gray}│${COLORS.reset}`);
|
|
839
|
+
let aggregateStr = ` ${COLORS.cyan}Σ${COLORS.reset} `;
|
|
840
|
+
aggregateStr += `${COLORS.dim}CPU:${COLORS.reset}${totalCpu.toFixed(1)}%`;
|
|
841
|
+
aggregateStr += ` ${COLORS.dim}RAM:${COLORS.reset}${totalMem.toFixed(1)}MB`;
|
|
842
|
+
if (totalBytesSent > 0 || totalBytesReceived > 0) {
|
|
843
|
+
aggregateStr += ` ${COLORS.dim}NET:${COLORS.reset}${COLORS.cyan}↑${this.formatBytes(totalBytesSent)} ↓${this.formatBytes(totalBytesReceived)}${COLORS.reset}`;
|
|
844
|
+
}
|
|
845
|
+
parts.push(aggregateStr);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
getAggregateMetrics() {
|
|
799
849
|
let totalCpu = 0;
|
|
800
850
|
let totalMem = 0;
|
|
801
851
|
let totalBytesSent = 0;
|
|
802
852
|
let totalBytesReceived = 0;
|
|
853
|
+
|
|
854
|
+
// Aggregate metrics (using interpolated values for smooth display)
|
|
803
855
|
for (const data of this.interpolatedMetrics.values()) {
|
|
804
|
-
if (data.exists
|
|
805
|
-
|
|
806
|
-
totalMem += data.current.memoryMB;
|
|
807
|
-
totalBytesSent += data.network?.bytesSent || 0;
|
|
808
|
-
totalBytesReceived += data.network?.bytesReceived || 0;
|
|
856
|
+
if (!data.exists || !data.current) {
|
|
857
|
+
continue;
|
|
809
858
|
}
|
|
810
|
-
}
|
|
811
859
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
aggregateStr += ` ${COLORS.dim}RAM:${COLORS.reset}${totalMem.toFixed(1)}MB`;
|
|
817
|
-
if (totalBytesSent > 0 || totalBytesReceived > 0) {
|
|
818
|
-
aggregateStr += ` ${COLORS.dim}NET:${COLORS.reset}${COLORS.cyan}↑${this.formatBytes(totalBytesSent)} ↓${this.formatBytes(totalBytesReceived)}${COLORS.reset}`;
|
|
819
|
-
}
|
|
820
|
-
parts.push(aggregateStr);
|
|
860
|
+
totalCpu += data.current.cpuPercent;
|
|
861
|
+
totalMem += data.current.memoryMB;
|
|
862
|
+
totalBytesSent += data.network?.bytesSent || 0;
|
|
863
|
+
totalBytesReceived += data.network?.bytesReceived || 0;
|
|
821
864
|
}
|
|
822
865
|
|
|
823
|
-
|
|
866
|
+
return { totalCpu, totalMem, totalBytesSent, totalBytesReceived };
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
padSummaryLine(parts, width) {
|
|
824
870
|
const content = parts.join('');
|
|
825
871
|
const contentLen = this.stripAnsi(content).length;
|
|
826
872
|
const padding = Math.max(0, width - contentLen - 1);
|
|
@@ -286,47 +286,88 @@ class SubClusterWrapper {
|
|
|
286
286
|
_buildChildContext(triggeringMessage) {
|
|
287
287
|
const parentTopics = this.config.contextStrategy?.parentTopics || [];
|
|
288
288
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
});
|
|
289
|
+
const lines = [
|
|
290
|
+
'# Child Cluster Context',
|
|
291
|
+
'',
|
|
292
|
+
`Parent Cluster: ${this.parentCluster.id}`,
|
|
293
|
+
`SubCluster ID: ${this.id}`,
|
|
294
|
+
`Iteration: ${this.iteration}`,
|
|
295
|
+
'',
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
this._appendParentTopicContext(lines, parentTopics);
|
|
299
|
+
this._appendTriggeringMessageContext(lines, triggeringMessage);
|
|
300
|
+
|
|
301
|
+
return lines.join('\n');
|
|
302
|
+
}
|
|
304
303
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
}
|
|
304
|
+
_appendParentTopicContext(lines, parentTopics) {
|
|
305
|
+
if (parentTopics.length === 0) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
lines.push('## Parent Cluster Messages', '');
|
|
310
|
+
|
|
311
|
+
for (const topic of parentTopics) {
|
|
312
|
+
const topicLines = this._buildTopicContextLines(topic);
|
|
313
|
+
if (topicLines.length === 0) {
|
|
314
|
+
continue;
|
|
318
315
|
}
|
|
316
|
+
|
|
317
|
+
lines.push(...topicLines);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
_buildTopicContextLines(topic) {
|
|
322
|
+
const messages = this.messageBus.query({
|
|
323
|
+
cluster_id: this.parentCluster.id,
|
|
324
|
+
topic,
|
|
325
|
+
limit: 10,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if (messages.length === 0) {
|
|
329
|
+
return [];
|
|
319
330
|
}
|
|
320
331
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
context += `\n${triggeringMessage.content.text}\n`;
|
|
332
|
+
const lines = [`### Topic: ${topic}`, ''];
|
|
333
|
+
|
|
334
|
+
for (const message of messages) {
|
|
335
|
+
lines.push(...this._buildMessageContextLines(message));
|
|
336
|
+
lines.push('');
|
|
327
337
|
}
|
|
328
338
|
|
|
329
|
-
return
|
|
339
|
+
return lines;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
_buildMessageContextLines(message) {
|
|
343
|
+
const lines = [`[${new Date(message.timestamp).toISOString()}] ${message.sender}:`];
|
|
344
|
+
const text = message.content?.text;
|
|
345
|
+
const data = message.content?.data;
|
|
346
|
+
|
|
347
|
+
if (text) {
|
|
348
|
+
lines.push(text);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (data) {
|
|
352
|
+
lines.push(`Data: ${JSON.stringify(data, null, 2)}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return lines;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
_appendTriggeringMessageContext(lines, triggeringMessage) {
|
|
359
|
+
lines.push(
|
|
360
|
+
'',
|
|
361
|
+
'## Triggering Message',
|
|
362
|
+
'',
|
|
363
|
+
`Topic: ${triggeringMessage.topic}`,
|
|
364
|
+
`Sender: ${triggeringMessage.sender}`
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
const text = triggeringMessage.content?.text;
|
|
368
|
+
if (text) {
|
|
369
|
+
lines.push('', text);
|
|
370
|
+
}
|
|
330
371
|
}
|
|
331
372
|
|
|
332
373
|
/**
|
package/src/template-resolver.js
CHANGED
|
@@ -272,25 +272,28 @@ class TemplateResolver {
|
|
|
272
272
|
* @returns {any}
|
|
273
273
|
*/
|
|
274
274
|
_parseValue(str) {
|
|
275
|
-
|
|
275
|
+
const trimmed = str.trim();
|
|
276
276
|
|
|
277
277
|
// String literals (single or double quotes)
|
|
278
|
-
if (
|
|
279
|
-
|
|
278
|
+
if (
|
|
279
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
280
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
281
|
+
) {
|
|
282
|
+
return trimmed.slice(1, -1);
|
|
280
283
|
}
|
|
281
284
|
|
|
282
285
|
// Boolean literals
|
|
283
|
-
if (
|
|
284
|
-
if (
|
|
285
|
-
if (
|
|
286
|
+
if (trimmed === 'true') return true;
|
|
287
|
+
if (trimmed === 'false') return false;
|
|
288
|
+
if (trimmed === 'undefined' || trimmed === 'null') return undefined;
|
|
286
289
|
|
|
287
290
|
// Numbers
|
|
288
|
-
if (/^-?\d+(\.\d+)?$/.test(
|
|
289
|
-
return parseFloat(
|
|
291
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
292
|
+
return parseFloat(trimmed);
|
|
290
293
|
}
|
|
291
294
|
|
|
292
295
|
// Return as-is for other cases
|
|
293
|
-
return
|
|
296
|
+
return trimmed;
|
|
294
297
|
}
|
|
295
298
|
|
|
296
299
|
/**
|
package/src/tui/data-poller.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
const pidusage = require('pidusage');
|
|
12
12
|
const Ledger = require('../ledger');
|
|
13
|
+
const fs = require('fs');
|
|
13
14
|
const path = require('path');
|
|
14
15
|
const os = require('os');
|
|
15
16
|
|
|
@@ -138,47 +139,11 @@ class DataPoller {
|
|
|
138
139
|
try {
|
|
139
140
|
const clusters = this.orchestrator.listClusters();
|
|
140
141
|
const stats = {};
|
|
142
|
+
const pids = this._collectClusterPids(clusters);
|
|
141
143
|
|
|
142
|
-
// Collect all PIDs from all agents
|
|
143
|
-
const pids = [];
|
|
144
|
-
for (const cluster of clusters) {
|
|
145
|
-
try {
|
|
146
|
-
const status = this.orchestrator.getStatus(cluster.id);
|
|
147
|
-
for (const agent of status.agents || []) {
|
|
148
|
-
if (agent.pid) {
|
|
149
|
-
pids.push(agent.pid);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
} catch {
|
|
153
|
-
// Skip clusters that error
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Get stats for all PIDs
|
|
159
144
|
if (pids.length > 0) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
// Convert to map format: pid -> { cpu, memory }
|
|
164
|
-
for (const pid of pids) {
|
|
165
|
-
if (pidStats[pid]) {
|
|
166
|
-
stats[pid] = {
|
|
167
|
-
cpu: pidStats[pid].cpu || 0,
|
|
168
|
-
memory: pidStats[pid].memory || 0,
|
|
169
|
-
};
|
|
170
|
-
} else {
|
|
171
|
-
// Process died - set to zero
|
|
172
|
-
stats[pid] = { cpu: 0, memory: 0 };
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
} catch {
|
|
176
|
-
// pidusage throws if any process is dead
|
|
177
|
-
// Set all to zero and continue
|
|
178
|
-
for (const pid of pids) {
|
|
179
|
-
stats[pid] = { cpu: 0, memory: 0 };
|
|
180
|
-
}
|
|
181
|
-
}
|
|
145
|
+
const pidStats = await this._safePidUsage(pids);
|
|
146
|
+
this._populatePidStats(stats, pids, pidStats);
|
|
182
147
|
}
|
|
183
148
|
|
|
184
149
|
this.onUpdate({
|
|
@@ -198,54 +163,36 @@ class DataPoller {
|
|
|
198
163
|
*/
|
|
199
164
|
_watchForNewClusters() {
|
|
200
165
|
this.watchForNewClustersStopFn = this.orchestrator.watchForNewClusters((cluster) => {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
// Only load if database file exists
|
|
209
|
-
const fs = require('fs');
|
|
210
|
-
if (!fs.existsSync(dbPath)) {
|
|
211
|
-
return; // Skip non-existent ledgers
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const ledger = new Ledger(dbPath);
|
|
215
|
-
this.ledgers.set(cluster.id, ledger);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Start streaming messages
|
|
219
|
-
this._streamLedgerMessages(cluster.id);
|
|
166
|
+
// Lazy load ledger only when we need to stream messages
|
|
167
|
+
// This avoids loading all ledgers on startup
|
|
168
|
+
const ledger = this._loadLedgerForCluster(cluster, {
|
|
169
|
+
label: 'cluster',
|
|
170
|
+
requireExisting: true,
|
|
171
|
+
});
|
|
220
172
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
type: 'new_cluster',
|
|
224
|
-
cluster,
|
|
225
|
-
});
|
|
226
|
-
} catch (error) {
|
|
227
|
-
console.error(
|
|
228
|
-
`[DataPoller] Failed to load ledger for cluster ${cluster.id}:`,
|
|
229
|
-
error.message
|
|
230
|
-
);
|
|
173
|
+
if (!ledger) {
|
|
174
|
+
return;
|
|
231
175
|
}
|
|
176
|
+
|
|
177
|
+
// Start streaming messages
|
|
178
|
+
this._streamLedgerMessages(cluster.id);
|
|
179
|
+
|
|
180
|
+
// Emit update about new cluster
|
|
181
|
+
this.onUpdate({
|
|
182
|
+
type: 'new_cluster',
|
|
183
|
+
cluster,
|
|
184
|
+
});
|
|
232
185
|
}, 2000);
|
|
233
186
|
|
|
234
187
|
// Also load ledgers for all existing clusters
|
|
235
188
|
const existingClusters = this.orchestrator.listClusters();
|
|
236
189
|
for (const cluster of existingClusters) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const ledger = new Ledger(dbPath);
|
|
241
|
-
this.ledgers.set(cluster.id, ledger);
|
|
242
|
-
this._streamLedgerMessages(cluster.id);
|
|
243
|
-
} catch (error) {
|
|
244
|
-
console.error(
|
|
245
|
-
`[DataPoller] Failed to load ledger for existing cluster ${cluster.id}:`,
|
|
246
|
-
error.message
|
|
247
|
-
);
|
|
190
|
+
const ledger = this._loadLedgerForCluster(cluster, { label: 'existing cluster' });
|
|
191
|
+
if (!ledger) {
|
|
192
|
+
continue;
|
|
248
193
|
}
|
|
194
|
+
|
|
195
|
+
this._streamLedgerMessages(cluster.id);
|
|
249
196
|
}
|
|
250
197
|
}
|
|
251
198
|
|
|
@@ -295,31 +242,108 @@ class DataPoller {
|
|
|
295
242
|
const clusters = this.orchestrator.listClusters();
|
|
296
243
|
|
|
297
244
|
for (const cluster of clusters) {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
for (const agent of status.agents || []) {
|
|
302
|
-
if (agent.pid) {
|
|
303
|
-
try {
|
|
304
|
-
const pidStat = await pidusage(agent.pid);
|
|
305
|
-
stats[agent.pid] = {
|
|
306
|
-
cpu: pidStat.cpu || 0,
|
|
307
|
-
memory: pidStat.memory || 0,
|
|
308
|
-
};
|
|
309
|
-
} catch {
|
|
310
|
-
// Process died - set to zero
|
|
311
|
-
stats[agent.pid] = { cpu: 0, memory: 0 };
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
} catch {
|
|
316
|
-
// Skip clusters that error
|
|
317
|
-
continue;
|
|
245
|
+
const pids = this._getClusterAgentPids(cluster);
|
|
246
|
+
for (const pid of pids) {
|
|
247
|
+
stats[pid] = await this._getSinglePidStat(pid);
|
|
318
248
|
}
|
|
319
249
|
}
|
|
320
250
|
|
|
321
251
|
return stats;
|
|
322
252
|
}
|
|
253
|
+
|
|
254
|
+
_collectClusterPids(clusters) {
|
|
255
|
+
const pids = [];
|
|
256
|
+
|
|
257
|
+
for (const cluster of clusters) {
|
|
258
|
+
pids.push(...this._getClusterAgentPids(cluster));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return pids;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
_getClusterAgentPids(cluster) {
|
|
265
|
+
try {
|
|
266
|
+
const status = this.orchestrator.getStatus(cluster.id);
|
|
267
|
+
const pids = [];
|
|
268
|
+
|
|
269
|
+
for (const agent of status.agents || []) {
|
|
270
|
+
if (agent.pid) {
|
|
271
|
+
pids.push(agent.pid);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return pids;
|
|
276
|
+
} catch {
|
|
277
|
+
// Skip clusters that error
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async _getSinglePidStat(pid) {
|
|
283
|
+
try {
|
|
284
|
+
const pidStat = await pidusage(pid);
|
|
285
|
+
return {
|
|
286
|
+
cpu: pidStat.cpu || 0,
|
|
287
|
+
memory: pidStat.memory || 0,
|
|
288
|
+
};
|
|
289
|
+
} catch {
|
|
290
|
+
// Process died - set to zero
|
|
291
|
+
return { cpu: 0, memory: 0 };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async _safePidUsage(pids) {
|
|
296
|
+
try {
|
|
297
|
+
return await pidusage(pids);
|
|
298
|
+
} catch {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
_populatePidStats(stats, pids, pidStats) {
|
|
304
|
+
for (const pid of pids) {
|
|
305
|
+
const entry = pidStats?.[pid];
|
|
306
|
+
stats[pid] = {
|
|
307
|
+
cpu: entry?.cpu || 0,
|
|
308
|
+
memory: entry?.memory || 0,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
_loadLedgerForCluster(cluster, options) {
|
|
314
|
+
const { label, requireExisting = false } = options;
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
return this._ensureLedger(cluster.id, { requireExisting });
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.error(
|
|
320
|
+
`[DataPoller] Failed to load ledger for ${label} ${cluster.id}:`,
|
|
321
|
+
error.message
|
|
322
|
+
);
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
_ensureLedger(clusterId, { requireExisting = false } = {}) {
|
|
328
|
+
const existing = this.ledgers.get(clusterId);
|
|
329
|
+
if (existing) {
|
|
330
|
+
return existing;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const dbPath = this._getLedgerPath(clusterId);
|
|
334
|
+
if (requireExisting && !fs.existsSync(dbPath)) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const ledger = new Ledger(dbPath);
|
|
339
|
+
this.ledgers.set(clusterId, ledger);
|
|
340
|
+
return ledger;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
_getLedgerPath(clusterId) {
|
|
344
|
+
const storageDir = this.orchestrator.storageDir || path.join(os.homedir(), '.zeroshot');
|
|
345
|
+
return path.join(storageDir, `${clusterId}.db`);
|
|
346
|
+
}
|
|
323
347
|
}
|
|
324
348
|
|
|
325
349
|
module.exports = DataPoller;
|
package/src/tui/formatters.js
CHANGED
|
@@ -64,12 +64,13 @@ const formatCPU = (percent) => {
|
|
|
64
64
|
if (typeof percent !== 'number' || percent < 0) return '0.0%';
|
|
65
65
|
|
|
66
66
|
// Assert normalized range (should never exceed 100% after per-core normalization)
|
|
67
|
-
|
|
67
|
+
let normalizedPercent = percent;
|
|
68
|
+
if (normalizedPercent > 100) {
|
|
68
69
|
console.warn(`[formatCPU] CPU percent ${percent}% exceeds 100% - normalization bug?`);
|
|
69
|
-
|
|
70
|
+
normalizedPercent = 100;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
return `${
|
|
73
|
+
return `${normalizedPercent.toFixed(1)}%`;
|
|
73
74
|
};
|
|
74
75
|
|
|
75
76
|
/**
|