bosun 0.36.2 → 0.36.4
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/agent-prompts.mjs +95 -0
- package/analyze-agent-work-helpers.mjs +308 -0
- package/analyze-agent-work.mjs +926 -0
- package/autofix.mjs +2 -0
- package/bosun.schema.json +101 -3
- package/codex-shell.mjs +85 -10
- package/desktop/main.mjs +871 -48
- package/desktop/preload.mjs +54 -1
- package/desktop-shortcut.mjs +90 -11
- package/git-editor-fix.mjs +273 -0
- package/mcp-registry.mjs +579 -0
- package/meeting-workflow-service.mjs +631 -0
- package/monitor.mjs +18 -103
- package/package.json +21 -2
- package/primary-agent.mjs +32 -12
- package/session-tracker.mjs +68 -0
- package/setup-web-server.mjs +20 -10
- package/setup.mjs +376 -83
- package/startup-service.mjs +51 -6
- package/stream-resilience.mjs +17 -7
- package/ui/app.js +164 -4
- package/ui/components/agent-selector.js +145 -1
- package/ui/components/chat-view.js +161 -15
- package/ui/components/session-list.js +2 -2
- package/ui/components/shared.js +188 -15
- package/ui/modules/icons.js +13 -0
- package/ui/modules/utils.js +44 -0
- package/ui/modules/voice-client-sdk.js +733 -0
- package/ui/modules/voice-overlay.js +128 -15
- package/ui/modules/voice.js +15 -6
- package/ui/setup.html +281 -81
- package/ui/styles/components.css +99 -3
- package/ui/styles/sessions.css +122 -14
- package/ui/styles.css +14 -0
- package/ui/tabs/agents.js +1 -1
- package/ui/tabs/chat.js +123 -14
- package/ui/tabs/control.js +16 -22
- package/ui/tabs/dashboard.js +85 -8
- package/ui/tabs/library.js +113 -17
- package/ui/tabs/settings.js +116 -2
- package/ui/tabs/tasks.js +388 -39
- package/ui/tabs/telemetry.js +0 -1
- package/ui/tabs/workflows.js +4 -0
- package/ui-server.mjs +400 -22
- package/update-check.mjs +41 -13
- package/voice-action-dispatcher.mjs +844 -0
- package/voice-agents-sdk.mjs +664 -0
- package/voice-auth-manager.mjs +164 -0
- package/voice-relay.mjs +1194 -0
- package/voice-tools.mjs +914 -0
- package/workflow-templates/agents.mjs +6 -2
- package/workflow-templates/github.mjs +154 -12
- package/workflow-templates.mjs +3 -0
- package/github-reconciler.mjs +0 -506
- package/merge-strategy.mjs +0 -1210
- package/pr-cleanup-daemon.mjs +0 -992
- package/workspace-reaper.mjs +0 -405
package/agent-prompts.mjs
CHANGED
|
@@ -125,6 +125,18 @@ const PROMPT_DEFS = [
|
|
|
125
125
|
description:
|
|
126
126
|
"Front-end specialist agent with screenshot-based validation and visual verification.",
|
|
127
127
|
},
|
|
128
|
+
{
|
|
129
|
+
key: "voiceAgent",
|
|
130
|
+
filename: "voice-agent.md",
|
|
131
|
+
description:
|
|
132
|
+
"Voice agent system prompt for real-time voice sessions with action dispatch.",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
key: "voiceAgentCompact",
|
|
136
|
+
filename: "voice-agent-compact.md",
|
|
137
|
+
description:
|
|
138
|
+
"Compact voice agent prompt for bandwidth-constrained or low-latency sessions.",
|
|
139
|
+
},
|
|
128
140
|
];
|
|
129
141
|
|
|
130
142
|
export const AGENT_PROMPT_DEFINITIONS = Object.freeze(
|
|
@@ -922,6 +934,89 @@ requirements before the task is marked as done.
|
|
|
922
934
|
- Working Directory: {{WORKTREE_PATH}}
|
|
923
935
|
|
|
924
936
|
{{COAUTHOR_INSTRUCTION}}
|
|
937
|
+
`,
|
|
938
|
+
voiceAgent: `# Bosun Voice Agent
|
|
939
|
+
|
|
940
|
+
You are **Bosun**, a voice-first assistant for the VirtEngine development platform.
|
|
941
|
+
You interact with developers through real-time voice conversations and have **full access**
|
|
942
|
+
to the Bosun workspace, task board, coding agents, and system operations.
|
|
943
|
+
|
|
944
|
+
## Core Capabilities
|
|
945
|
+
|
|
946
|
+
You can do everything Bosun can — through voice. This includes:
|
|
947
|
+
- **Task management**: List, create, update, delete, search, and comment on tasks
|
|
948
|
+
- **Agent delegation**: Send work to coding agents (Codex, Copilot, Claude, Gemini, OpenCode)
|
|
949
|
+
- **Agent steering**: Use /ask (read-only), /agent (code changes), or /plan (architecture)
|
|
950
|
+
- **System monitoring**: Check fleet status, agent health, system configuration
|
|
951
|
+
- **Workspace navigation**: Read files, list directories, search code
|
|
952
|
+
- **Workflow management**: List and inspect workflow templates
|
|
953
|
+
- **Skills & prompts**: Browse the knowledge base and prompt library
|
|
954
|
+
|
|
955
|
+
## How Actions Work
|
|
956
|
+
|
|
957
|
+
When the user asks you to do something, you perform it by returning a JSON action intent.
|
|
958
|
+
Bosun processes the action directly via JavaScript (no MCP bridge needed) and returns the result.
|
|
959
|
+
You then speak the result to the user naturally.
|
|
960
|
+
|
|
961
|
+
### Action Format
|
|
962
|
+
\`\`\`json
|
|
963
|
+
{ "action": "task.list", "params": { "status": "todo" } }
|
|
964
|
+
\`\`\`
|
|
965
|
+
|
|
966
|
+
### Multiple Actions
|
|
967
|
+
\`\`\`json
|
|
968
|
+
{ "action": "batch", "params": { "actions": [
|
|
969
|
+
{ "action": "task.stats", "params": {} },
|
|
970
|
+
{ "action": "agent.status", "params": {} }
|
|
971
|
+
] } }
|
|
972
|
+
\`\`\`
|
|
973
|
+
|
|
974
|
+
{{VOICE_ACTION_MANIFEST}}
|
|
975
|
+
|
|
976
|
+
## Agent Delegation
|
|
977
|
+
|
|
978
|
+
When users need code written, files modified, bugs debugged, or PRs created:
|
|
979
|
+
1. Use \`agent.delegate\` with a detailed message
|
|
980
|
+
2. Choose the right mode: "ask" for questions, "agent" for code changes, "plan" for architecture
|
|
981
|
+
3. You can specify which executor to use, or let the default handle it
|
|
982
|
+
|
|
983
|
+
Examples:
|
|
984
|
+
- "Fix the login bug" → \`{ "action": "agent.code", "params": { "message": "Fix the login bug in auth.mjs" } }\`
|
|
985
|
+
- "How does the config system work?" → \`{ "action": "agent.ask", "params": { "message": "Explain the config system" } }\`
|
|
986
|
+
- "Plan a refactor of the voice module" → \`{ "action": "agent.plan", "params": { "message": "Plan refactoring voice-relay.mjs" } }\`
|
|
987
|
+
|
|
988
|
+
## Conversation Style
|
|
989
|
+
|
|
990
|
+
- Be **concise and conversational** — this is voice, not text.
|
|
991
|
+
- Lead with the answer, then add details if needed.
|
|
992
|
+
- For numbers, say them naturally: "You have 12 tasks in the backlog."
|
|
993
|
+
- When tasks or agents are busy, keep the user informed.
|
|
994
|
+
- For long outputs (code, logs), summarize the key points vocally.
|
|
995
|
+
- When delegating to an agent, let the user know: "I'm sending that to Codex now."
|
|
996
|
+
|
|
997
|
+
## Error Handling
|
|
998
|
+
|
|
999
|
+
If an action fails, explain what happened and suggest alternatives.
|
|
1000
|
+
Never show raw error objects — speak the issue naturally.
|
|
1001
|
+
|
|
1002
|
+
## Security
|
|
1003
|
+
|
|
1004
|
+
- Never expose API keys, tokens, or secrets in conversation.
|
|
1005
|
+
- Only execute safe operations via voice (reads, creates, delegates).
|
|
1006
|
+
- Dangerous operations (delete all tasks, force push) require explicit confirmation.
|
|
1007
|
+
`,
|
|
1008
|
+
voiceAgentCompact: `# Bosun Voice (Compact)
|
|
1009
|
+
|
|
1010
|
+
Voice assistant for VirtEngine. Access tasks, agents, workspace.
|
|
1011
|
+
|
|
1012
|
+
Return JSON actions: { "action": "<name>", "params": { ... } }
|
|
1013
|
+
|
|
1014
|
+
{{VOICE_ACTION_MANIFEST}}
|
|
1015
|
+
|
|
1016
|
+
Key actions: task.list, task.create, task.stats, agent.delegate, agent.ask, agent.plan,
|
|
1017
|
+
system.status, workspace.readFile, workspace.search.
|
|
1018
|
+
|
|
1019
|
+
Be concise. Lead with answers. Summarize long outputs.
|
|
925
1020
|
`,
|
|
926
1021
|
};
|
|
927
1022
|
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { classifyComplexity } from "./task-complexity.mjs";
|
|
2
|
+
|
|
3
|
+
export function normalizeTimestamp(value) {
|
|
4
|
+
const ts = Date.parse(value);
|
|
5
|
+
return Number.isFinite(ts) ? ts : null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function normalizeErrorFingerprint(message) {
|
|
9
|
+
const text = String(message || "unknown").trim().toLowerCase();
|
|
10
|
+
if (!text) return "unknown";
|
|
11
|
+
return text
|
|
12
|
+
.replace(/0x[0-9a-f]+/gi, "0x#")
|
|
13
|
+
.replace(/\d+(?:\.\d+)?/g, "#")
|
|
14
|
+
.replace(/\s+/g, " ")
|
|
15
|
+
.slice(0, 120);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function groupBy(array, keyFn) {
|
|
19
|
+
const groups = {};
|
|
20
|
+
for (const item of array) {
|
|
21
|
+
const key = typeof keyFn === "function" ? keyFn(item) : item[keyFn];
|
|
22
|
+
if (!groups[key]) groups[key] = [];
|
|
23
|
+
groups[key].push(item);
|
|
24
|
+
}
|
|
25
|
+
return groups;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function average(numbers) {
|
|
29
|
+
if (numbers.length === 0) return 0;
|
|
30
|
+
return numbers.reduce((a, b) => a + b, 0) / numbers.length;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function incrementCounter(target, key) {
|
|
34
|
+
const resolved = key || "unknown";
|
|
35
|
+
target[resolved] = (target[resolved] || 0) + 1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveKnownValue(...values) {
|
|
39
|
+
for (const value of values) {
|
|
40
|
+
if (value === null || value === undefined) continue;
|
|
41
|
+
const text = String(value).trim();
|
|
42
|
+
if (!text) continue;
|
|
43
|
+
if (text.toLowerCase() === "unknown") continue;
|
|
44
|
+
return text;
|
|
45
|
+
}
|
|
46
|
+
return "unknown";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildDistribution(counts, total) {
|
|
50
|
+
return Object.entries(counts)
|
|
51
|
+
.map(([label, count]) => ({
|
|
52
|
+
label,
|
|
53
|
+
count,
|
|
54
|
+
percent: total > 0 ? (count * 100.0) / total : 0,
|
|
55
|
+
}))
|
|
56
|
+
.sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const SIZE_LABEL_PATTERN = /\[(xs|s|m|l|xl|xxl|2xl)\]/i;
|
|
60
|
+
|
|
61
|
+
function extractSizeLabelFromTitle(title) {
|
|
62
|
+
const text = String(title || "");
|
|
63
|
+
const match = text.match(SIZE_LABEL_PATTERN);
|
|
64
|
+
if (!match) return "unknown";
|
|
65
|
+
const label = match[1].toLowerCase();
|
|
66
|
+
return label === "2xl" ? "xxl" : label;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function classifyComplexityBucket({ sizeLabel, title, description }) {
|
|
70
|
+
if (!description) return "unknown";
|
|
71
|
+
const normalizedSize = sizeLabel && sizeLabel !== "unknown" ? sizeLabel : null;
|
|
72
|
+
const result = classifyComplexity({
|
|
73
|
+
sizeLabel: normalizedSize,
|
|
74
|
+
title: title || "",
|
|
75
|
+
description: description || "",
|
|
76
|
+
});
|
|
77
|
+
return result?.tier || "unknown";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveNowTimestamp(now) {
|
|
81
|
+
if (now instanceof Date) return now.getTime();
|
|
82
|
+
if (typeof now === "number" && Number.isFinite(now)) return now;
|
|
83
|
+
if (typeof now === "string") {
|
|
84
|
+
const parsed = normalizeTimestamp(now);
|
|
85
|
+
if (parsed !== null) return parsed;
|
|
86
|
+
}
|
|
87
|
+
return Date.now();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function filterRecordsByWindow(
|
|
91
|
+
records,
|
|
92
|
+
{ days, now, timestampKey = "timestamp" } = {},
|
|
93
|
+
) {
|
|
94
|
+
if (!Array.isArray(records)) return [];
|
|
95
|
+
const windowDays = Number.isFinite(days) && days > 0 ? days : null;
|
|
96
|
+
if (!windowDays) return [...records];
|
|
97
|
+
const cutoff = resolveNowTimestamp(now) - windowDays * 24 * 60 * 60 * 1000;
|
|
98
|
+
return records.filter((record) => {
|
|
99
|
+
const ts = normalizeTimestamp(record?.[timestampKey]);
|
|
100
|
+
if (ts === null) return true;
|
|
101
|
+
return ts >= cutoff;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function buildErrorClusters(errors) {
|
|
106
|
+
if (!Array.isArray(errors) || errors.length === 0) return [];
|
|
107
|
+
|
|
108
|
+
const byFingerprint = groupBy(
|
|
109
|
+
errors,
|
|
110
|
+
(e) =>
|
|
111
|
+
e.data?.error_fingerprint ||
|
|
112
|
+
normalizeErrorFingerprint(e.data?.error_message),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return Object.entries(byFingerprint)
|
|
116
|
+
.map(([fingerprint, events]) => ({
|
|
117
|
+
fingerprint,
|
|
118
|
+
count: events.length,
|
|
119
|
+
affected_tasks: new Set(events.map((e) => e.task_id)).size,
|
|
120
|
+
affected_attempts: new Set(events.map((e) => e.attempt_id)).size,
|
|
121
|
+
first_seen: events[0].timestamp,
|
|
122
|
+
last_seen: events[events.length - 1].timestamp,
|
|
123
|
+
sample_message: events[0].data?.error_message || "",
|
|
124
|
+
categories: [
|
|
125
|
+
...new Set(events.map((e) => e.data?.error_category).filter(Boolean)),
|
|
126
|
+
],
|
|
127
|
+
}))
|
|
128
|
+
.sort((a, b) => b.count - a.count);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function buildTaskProfiles(metrics, errors) {
|
|
132
|
+
const taskProfiles = new Map();
|
|
133
|
+
const ensureTaskProfile = (taskId) => {
|
|
134
|
+
const key = taskId || "unknown";
|
|
135
|
+
if (!taskProfiles.has(key)) {
|
|
136
|
+
taskProfiles.set(key, {
|
|
137
|
+
task_id: key,
|
|
138
|
+
task_title: "",
|
|
139
|
+
task_description: "",
|
|
140
|
+
executor: "unknown",
|
|
141
|
+
model: "unknown",
|
|
142
|
+
durations: [],
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return taskProfiles.get(key);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
for (const metric of metrics) {
|
|
149
|
+
const profile = ensureTaskProfile(metric.task_id || "unknown");
|
|
150
|
+
if (metric.task_title && !profile.task_title) {
|
|
151
|
+
profile.task_title = metric.task_title;
|
|
152
|
+
}
|
|
153
|
+
if (metric.task_description && !profile.task_description) {
|
|
154
|
+
profile.task_description = metric.task_description;
|
|
155
|
+
}
|
|
156
|
+
if (metric.executor && profile.executor === "unknown") {
|
|
157
|
+
profile.executor = metric.executor;
|
|
158
|
+
}
|
|
159
|
+
if (metric.model && profile.model === "unknown") {
|
|
160
|
+
profile.model = metric.model;
|
|
161
|
+
}
|
|
162
|
+
if (metric.metrics?.duration_ms) {
|
|
163
|
+
profile.durations.push(metric.metrics.duration_ms);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const error of errors) {
|
|
168
|
+
const profile = ensureTaskProfile(error.task_id || "unknown");
|
|
169
|
+
if (error.task_title && !profile.task_title) {
|
|
170
|
+
profile.task_title = error.task_title;
|
|
171
|
+
}
|
|
172
|
+
if (error.task_description && !profile.task_description) {
|
|
173
|
+
profile.task_description = error.task_description;
|
|
174
|
+
}
|
|
175
|
+
if (error.executor && profile.executor === "unknown") {
|
|
176
|
+
profile.executor = error.executor;
|
|
177
|
+
}
|
|
178
|
+
if (error.model && profile.model === "unknown") {
|
|
179
|
+
profile.model = error.model;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const profile of taskProfiles.values()) {
|
|
184
|
+
profile.size_label = extractSizeLabelFromTitle(profile.task_title);
|
|
185
|
+
profile.complexity = classifyComplexityBucket({
|
|
186
|
+
sizeLabel: profile.size_label,
|
|
187
|
+
title: profile.task_title,
|
|
188
|
+
description: profile.task_description,
|
|
189
|
+
});
|
|
190
|
+
profile.avg_duration_ms =
|
|
191
|
+
profile.durations.length > 0 ? average(profile.durations) : 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return taskProfiles;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function buildErrorCorrelationEntries(errors, taskProfiles) {
|
|
198
|
+
const correlations = new Map();
|
|
199
|
+
const ensureCorrelation = (fingerprint) => {
|
|
200
|
+
if (!correlations.has(fingerprint)) {
|
|
201
|
+
correlations.set(fingerprint, {
|
|
202
|
+
fingerprint,
|
|
203
|
+
count: 0,
|
|
204
|
+
task_ids: new Set(),
|
|
205
|
+
by_executor: {},
|
|
206
|
+
by_size: {},
|
|
207
|
+
by_complexity: {},
|
|
208
|
+
sample_message: "",
|
|
209
|
+
first_seen_ts: null,
|
|
210
|
+
last_seen_ts: null,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return correlations.get(fingerprint);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
for (const error of errors) {
|
|
217
|
+
const fingerprint =
|
|
218
|
+
error.data?.error_fingerprint ||
|
|
219
|
+
normalizeErrorFingerprint(error.data?.error_message);
|
|
220
|
+
const entry = ensureCorrelation(fingerprint);
|
|
221
|
+
entry.count += 1;
|
|
222
|
+
entry.task_ids.add(error.task_id || "unknown");
|
|
223
|
+
if (!entry.sample_message && error.data?.error_message) {
|
|
224
|
+
entry.sample_message = error.data.error_message;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const timestamp = normalizeTimestamp(error.timestamp);
|
|
228
|
+
if (timestamp !== null) {
|
|
229
|
+
if (!entry.first_seen_ts || timestamp < entry.first_seen_ts) {
|
|
230
|
+
entry.first_seen_ts = timestamp;
|
|
231
|
+
}
|
|
232
|
+
if (!entry.last_seen_ts || timestamp > entry.last_seen_ts) {
|
|
233
|
+
entry.last_seen_ts = timestamp;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const profile = taskProfiles.get(error.task_id || "unknown");
|
|
238
|
+
const sizeLabel = resolveKnownValue(
|
|
239
|
+
profile?.size_label,
|
|
240
|
+
extractSizeLabelFromTitle(error.task_title),
|
|
241
|
+
);
|
|
242
|
+
const executor = resolveKnownValue(profile?.executor, error.executor);
|
|
243
|
+
const complexity = resolveKnownValue(profile?.complexity);
|
|
244
|
+
|
|
245
|
+
incrementCounter(entry.by_size, sizeLabel);
|
|
246
|
+
incrementCounter(entry.by_executor, executor);
|
|
247
|
+
incrementCounter(entry.by_complexity, complexity);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return correlations;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function buildErrorCorrelationSummary({
|
|
254
|
+
errors = [],
|
|
255
|
+
metrics = [],
|
|
256
|
+
windowDays = 7,
|
|
257
|
+
top = 10,
|
|
258
|
+
} = {}) {
|
|
259
|
+
const windowDaysResolved =
|
|
260
|
+
Number.isFinite(windowDays) && windowDays > 0 ? windowDays : 7;
|
|
261
|
+
const topLimit = Number.isFinite(top) && top > 0 ? top : 10;
|
|
262
|
+
const errorList = Array.isArray(errors) ? errors : [];
|
|
263
|
+
const metricList = Array.isArray(metrics) ? metrics : [];
|
|
264
|
+
|
|
265
|
+
const taskProfiles = buildTaskProfiles(metricList, errorList);
|
|
266
|
+
const correlations = buildErrorCorrelationEntries(errorList, taskProfiles);
|
|
267
|
+
const sorted = [...correlations.values()].sort((a, b) => b.count - a.count);
|
|
268
|
+
const topEntries = sorted.slice(0, topLimit);
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
window_days: windowDaysResolved,
|
|
272
|
+
total_errors: errorList.length,
|
|
273
|
+
total_fingerprints: correlations.size,
|
|
274
|
+
top: topLimit,
|
|
275
|
+
correlations: topEntries,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function buildErrorCorrelationJsonPayload(summary, { now } = {}) {
|
|
280
|
+
const safeSummary = summary && typeof summary === "object" ? summary : null;
|
|
281
|
+
const correlations = Array.isArray(safeSummary?.correlations)
|
|
282
|
+
? safeSummary.correlations
|
|
283
|
+
: [];
|
|
284
|
+
const generatedAt = new Date(resolveNowTimestamp(now)).toISOString();
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
generated_at: generatedAt,
|
|
288
|
+
window_days: safeSummary?.window_days ?? 7,
|
|
289
|
+
total_errors: safeSummary?.total_errors ?? 0,
|
|
290
|
+
total_fingerprints: safeSummary?.total_fingerprints ?? 0,
|
|
291
|
+
top: safeSummary?.top ?? correlations.length,
|
|
292
|
+
correlations: correlations.map((entry) => ({
|
|
293
|
+
fingerprint: entry.fingerprint,
|
|
294
|
+
count: entry.count,
|
|
295
|
+
task_count: entry.task_ids?.size || 0,
|
|
296
|
+
sample_message: entry.sample_message,
|
|
297
|
+
first_seen: entry.first_seen_ts
|
|
298
|
+
? new Date(entry.first_seen_ts).toISOString()
|
|
299
|
+
: null,
|
|
300
|
+
last_seen: entry.last_seen_ts
|
|
301
|
+
? new Date(entry.last_seen_ts).toISOString()
|
|
302
|
+
: null,
|
|
303
|
+
by_executor: buildDistribution(entry.by_executor, entry.count),
|
|
304
|
+
by_size: buildDistribution(entry.by_size, entry.count),
|
|
305
|
+
by_complexity: buildDistribution(entry.by_complexity, entry.count),
|
|
306
|
+
})),
|
|
307
|
+
};
|
|
308
|
+
}
|