feat: Phase 0+1 — repo sync, pino, lint fixes, core components
Phase 0: - Synced latest live-status.js from workspace (9928 bytes) - Fixed 43 lint issues: empty catch blocks, console statements - Added pino dependency - Created src/tool-labels.json with all known tool mappings - make check passes Phase 1 (Core Components): - src/config.js: env-var config with validation, throws on missing required vars - src/logger.js: pino singleton with child loggers, level validation - src/circuit-breaker.js: CLOSED/OPEN/HALF_OPEN state machine with callbacks - src/tool-labels.js: exact/prefix/regex tool->label resolver with external override - src/status-box.js: Mattermost post manager (keepAlive, throttle, retry, circuit breaker) - src/status-formatter.js: pure SessionState->text formatter (nested, compact) - src/health.js: HTTP health endpoint + metrics - src/status-watcher.js: JSONL file watcher (inotify, compaction detection, idle detection) Tests: - test/unit/config.test.js: 7 tests - test/unit/circuit-breaker.test.js: 12 tests - test/unit/logger.test.js: 5 tests - test/unit/status-formatter.test.js: 20 tests - test/unit/tool-labels.test.js: 15 tests All 59 unit tests pass. make check clean.
This commit is contained in:
142
src/status-formatter.js
Normal file
142
src/status-formatter.js
Normal file
@@ -0,0 +1,142 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* status-formatter.js — Pure function: SessionState -> formatted Mattermost text.
|
||||
*
|
||||
* Output format:
|
||||
* [ACTIVE] main | 38s
|
||||
* Reading live-status source code...
|
||||
* exec: ls /agents/sessions [OK]
|
||||
* Analyzing agent configurations...
|
||||
* Sub-agent: proj035-planner
|
||||
* Reading protocol...
|
||||
* [DONE] 28s
|
||||
* [DONE] 53s | 12.4k tokens
|
||||
*/
|
||||
|
||||
const MAX_STATUS_LINES = parseInt(process.env.MAX_STATUS_LINES, 10) || 20;
|
||||
const MAX_LINE_CHARS = 120;
|
||||
|
||||
/**
|
||||
* Format a SessionState into a Mattermost text string.
|
||||
*
|
||||
* @param {object} sessionState
|
||||
* @param {string} sessionState.sessionKey
|
||||
* @param {string} sessionState.status - 'active' | 'done' | 'error' | 'interrupted'
|
||||
* @param {number} sessionState.startTime - ms since epoch
|
||||
* @param {Array<string>} sessionState.lines - Status lines (most recent activity)
|
||||
* @param {Array<object>} [sessionState.children] - Child session states
|
||||
* @param {number} [sessionState.tokenCount] - Token count if available
|
||||
* @param {string} [sessionState.agentId] - Agent ID (e.g. "main")
|
||||
* @param {number} [sessionState.depth] - Nesting depth (0 = top-level)
|
||||
* @returns {string}
|
||||
*/
|
||||
function format(sessionState, opts = {}) {
|
||||
const maxLines = opts.maxLines || MAX_STATUS_LINES;
|
||||
const depth = sessionState.depth || 0;
|
||||
const indent = ' '.repeat(depth);
|
||||
|
||||
const lines = [];
|
||||
|
||||
// Header line
|
||||
const elapsed = formatElapsed(Date.now() - sessionState.startTime);
|
||||
const agentId = sessionState.agentId || extractAgentId(sessionState.sessionKey);
|
||||
const statusPrefix = statusIcon(sessionState.status);
|
||||
lines.push(`${indent}${statusPrefix} ${agentId} | ${elapsed}`);
|
||||
|
||||
// Status lines (trimmed to maxLines, most recent)
|
||||
const statusLines = (sessionState.lines || []).slice(-maxLines);
|
||||
for (const line of statusLines) {
|
||||
lines.push(`${indent} ${truncateLine(line)}`);
|
||||
}
|
||||
|
||||
// Child sessions (sub-agents)
|
||||
if (sessionState.children && sessionState.children.length > 0) {
|
||||
for (const child of sessionState.children) {
|
||||
const childLines = format(child, { maxLines: Math.floor(maxLines / 2), ...opts }).split('\n');
|
||||
lines.push(...childLines);
|
||||
}
|
||||
}
|
||||
|
||||
// Footer line (only for done/error/interrupted)
|
||||
if (sessionState.status !== 'active') {
|
||||
const tokenStr = sessionState.tokenCount
|
||||
? ` | ${formatTokens(sessionState.tokenCount)} tokens`
|
||||
: '';
|
||||
lines.push(`${indent} [${sessionState.status.toUpperCase()}] ${elapsed}${tokenStr}`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format elapsed milliseconds as human-readable string.
|
||||
* @param {number} ms
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatElapsed(ms) {
|
||||
if (ms < 0) ms = 0;
|
||||
const s = Math.floor(ms / 1000);
|
||||
const m = Math.floor(s / 60);
|
||||
const h = Math.floor(m / 60);
|
||||
if (h > 0) return `${h}h${m % 60}m`;
|
||||
if (m > 0) return `${m}m${s % 60}s`;
|
||||
return `${s}s`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format token count as compact string (e.g. 12400 -> "12.4k").
|
||||
* @param {number} count
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatTokens(count) {
|
||||
if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`;
|
||||
if (count >= 1000) return `${(count / 1000).toFixed(1)}k`;
|
||||
return String(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status icon prefix.
|
||||
* @param {string} status
|
||||
* @returns {string}
|
||||
*/
|
||||
function statusIcon(status) {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return '[ACTIVE]';
|
||||
case 'done':
|
||||
return '[DONE]';
|
||||
case 'error':
|
||||
return '[ERROR]';
|
||||
case 'interrupted':
|
||||
return '[INTERRUPTED]';
|
||||
default:
|
||||
return '[UNKNOWN]';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate a line to MAX_LINE_CHARS.
|
||||
* @param {string} line
|
||||
* @returns {string}
|
||||
*/
|
||||
function truncateLine(line) {
|
||||
if (line.length <= MAX_LINE_CHARS) return line;
|
||||
return line.slice(0, MAX_LINE_CHARS - 3) + '...';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract agent ID from session key.
|
||||
* Session key format: "agent:main:mattermost:channel:abc123:thread:xyz"
|
||||
* @param {string} sessionKey
|
||||
* @returns {string}
|
||||
*/
|
||||
function extractAgentId(sessionKey) {
|
||||
if (!sessionKey) return 'unknown';
|
||||
const parts = sessionKey.split(':');
|
||||
// "agent:main:..." -> "main"
|
||||
if (parts[0] === 'agent' && parts[1]) return parts[1];
|
||||
return sessionKey.split(':')[0] || 'unknown';
|
||||
}
|
||||
|
||||
module.exports = { format, formatElapsed, formatTokens, statusIcon, truncateLine, extractAgentId };
|
||||
Reference in New Issue
Block a user