bosun 0.41.10 → 0.42.1
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/.env.example +12 -0
- package/README.md +2 -0
- package/agent/agent-hooks.mjs +56 -1
- package/agent/agent-pool.mjs +34 -1
- package/agent/agent-work-report.mjs +89 -3
- package/agent/analyze-agent-work-helpers.mjs +14 -0
- package/agent/analyze-agent-work.mjs +23 -3
- package/agent/hook-library.mjs +1383 -0
- package/agent/hook-profiles.mjs +55 -3
- package/agent/primary-agent.mjs +142 -0
- package/bosun-tui.mjs +4 -3
- package/bosun.schema.json +1 -1
- package/config/config.mjs +58 -0
- package/config/workspace-health.mjs +36 -6
- package/git/diff-stats.mjs +550 -124
- package/github/github-app-auth.mjs +9 -5
- package/infra/library-manager.mjs +4 -2
- package/infra/maintenance.mjs +13 -6
- package/infra/monitor.mjs +398 -10
- package/infra/runtime-accumulator.mjs +9 -1
- package/infra/session-tracker.mjs +51 -1
- package/infra/tui-bridge.mjs +415 -0
- package/infra/worktree-recovery-state.mjs +159 -0
- package/kanban/kanban-adapter.mjs +41 -8
- package/package.json +140 -135
- package/server/ui-server.mjs +893 -65
- package/shell/codex-config.mjs +34 -8
- package/task/task-executor.mjs +1 -1
- package/telegram/telegram-bot.mjs +267 -18
- package/tools/vitest-runner.mjs +108 -0
- package/tui/app.mjs +252 -148
- package/tui/components/status-header.mjs +88 -131
- package/tui/lib/ws-bridge.mjs +125 -35
- package/tui/screens/agents-screen-helpers.mjs +219 -0
- package/tui/screens/agents.mjs +287 -270
- package/tui/screens/status.mjs +51 -189
- package/tui/screens/tasks.mjs +41 -253
- package/ui/app.js +52 -23
- package/ui/components/chat-view.js +263 -84
- package/ui/components/diff-viewer.js +324 -140
- package/ui/components/kanban-board.js +13 -9
- package/ui/components/session-list.js +111 -41
- package/ui/demo-defaults.js +281 -17
- package/ui/demo.html +6 -0
- package/ui/modules/session-api.js +320 -5
- package/ui/modules/stream-timeline.js +356 -0
- package/ui/modules/telegram.js +5 -2
- package/ui/modules/worktree-recovery.js +85 -0
- package/ui/styles.css +44 -0
- package/ui/tabs/chat.js +19 -4
- package/ui/tabs/dashboard.js +22 -0
- package/ui/tabs/infra.js +25 -0
- package/ui/tabs/tasks.js +119 -11
- package/ui/tabs/workflows.js +260 -10
- package/voice/voice-auth-manager.mjs +10 -5
- package/workflow/execution-ledger.mjs +140 -0
- package/workflow/workflow-engine.mjs +731 -27
- package/workflow/workflow-nodes.mjs +868 -53
- package/workflow/workflow-templates.mjs +4 -0
- package/workflow-templates/github.mjs +2 -1
- package/workflow-templates/planning.mjs +2 -1
- package/workflow-templates/task-batch.mjs +9 -8
- package/workflow-templates/task-lifecycle.mjs +51 -3
- package/workspace/shared-knowledge.mjs +409 -155
package/.env.example
CHANGED
|
@@ -1039,6 +1039,18 @@ COPILOT_CLOUD_DISABLED=true
|
|
|
1039
1039
|
# WORKFLOW_AUTOMATION_ENABLED=true
|
|
1040
1040
|
# Optional dedup window to avoid event storms (milliseconds).
|
|
1041
1041
|
# WORKFLOW_EVENT_DEDUP_WINDOW_MS=15000
|
|
1042
|
+
# Monitor self-healing retry policy for startup unstick operations
|
|
1043
|
+
# (stale dispatch polling and workflow-history resume).
|
|
1044
|
+
# Max bounded retry attempts before terminal escalation.
|
|
1045
|
+
# WORKFLOW_RECOVERY_MAX_ATTEMPTS=5
|
|
1046
|
+
# Escalation warning threshold for repeated failures (must be <= max attempts).
|
|
1047
|
+
# WORKFLOW_RECOVERY_ESCALATION_THRESHOLD=3
|
|
1048
|
+
# Exponential backoff base delay in ms.
|
|
1049
|
+
# WORKFLOW_RECOVERY_BACKOFF_BASE_MS=5000
|
|
1050
|
+
# Exponential backoff maximum delay in ms.
|
|
1051
|
+
# WORKFLOW_RECOVERY_BACKOFF_MAX_MS=60000
|
|
1052
|
+
# Random jitter ratio (0.0-0.9) applied to backoff to prevent retry storms.
|
|
1053
|
+
# WORKFLOW_RECOVERY_BACKOFF_JITTER_RATIO=0.2
|
|
1042
1054
|
|
|
1043
1055
|
# ─── GitHub Issue Reconciler ─────────────────────────────────────────────────
|
|
1044
1056
|
# Periodically reconciles open GitHub issues against open/merged PRs.
|
package/README.md
CHANGED
|
@@ -144,6 +144,8 @@ Set `primaryAgent` in `.bosun/bosun.config.json` or choose an executor preset du
|
|
|
144
144
|
- `bosun --daemon --sentinel` starts daemon + sentinel together (recommended for unattended operation).
|
|
145
145
|
- `bosun --terminate` is the clean reset command when you suspect stale/ghost processes.
|
|
146
146
|
|
|
147
|
+
Telegram operators can pull the weekly agent work summary with `/weekly [days]` or `/report weekly [days]`. To post it automatically once per week, set `TELEGRAM_WEEKLY_REPORT_ENABLED=true` together with `TELEGRAM_WEEKLY_REPORT_DAY`, `TELEGRAM_WEEKLY_REPORT_HOUR`, and optional `TELEGRAM_WEEKLY_REPORT_DAYS`.
|
|
148
|
+
|
|
147
149
|
## Documentation
|
|
148
150
|
|
|
149
151
|
**Published docs (website):** https://bosun.engineer/docs/
|
package/agent/agent-hooks.mjs
CHANGED
|
@@ -144,7 +144,7 @@ export const HOOK_EVENTS = Object.freeze([
|
|
|
144
144
|
* Canonical SDK names.
|
|
145
145
|
* @type {readonly string[]}
|
|
146
146
|
*/
|
|
147
|
-
const VALID_SDKS = Object.freeze(["codex", "copilot", "claude", "opencode"]);
|
|
147
|
+
const VALID_SDKS = Object.freeze(["codex", "copilot", "claude", "opencode", "gemini"]);
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
150
|
* Wildcard indicating a hook applies to all SDKs.
|
|
@@ -1198,3 +1198,58 @@ function _truncate(str, maxLen) {
|
|
|
1198
1198
|
if (!str || str.length <= maxLen) return str ?? "";
|
|
1199
1199
|
return str.slice(0, maxLen) + "\n... (truncated)";
|
|
1200
1200
|
}
|
|
1201
|
+
|
|
1202
|
+
// ── Hook Library Integration ────────────────────────────────────────────────
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* Register hooks from the hook-library into the agent-hooks runtime registry.
|
|
1206
|
+
* This bridges the declarative hook catalog with the execution engine.
|
|
1207
|
+
*
|
|
1208
|
+
* @param {Record<string, Array<{id: string, command: string, description?: string, timeout?: number, blocking?: boolean, sdks?: string[], builtin?: boolean, retryable?: boolean, maxRetries?: number, env?: Record<string,string>}>>} hooksByEvent
|
|
1209
|
+
* - Output of getHooksForRegistration() from hook-library.mjs
|
|
1210
|
+
* @returns {{ registered: number, skipped: number }}
|
|
1211
|
+
*/
|
|
1212
|
+
export function registerLibraryHooks(hooksByEvent) {
|
|
1213
|
+
let registered = 0;
|
|
1214
|
+
let skipped = 0;
|
|
1215
|
+
|
|
1216
|
+
if (!hooksByEvent || typeof hooksByEvent !== "object") {
|
|
1217
|
+
return { registered, skipped };
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
for (const [event, hooks] of Object.entries(hooksByEvent)) {
|
|
1221
|
+
if (!HOOK_EVENTS.includes(event)) {
|
|
1222
|
+
console.warn(`${TAG} unknown hook event from library: ${event}`);
|
|
1223
|
+
skipped += hooks.length;
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
for (const hook of hooks) {
|
|
1228
|
+
// Skip if a hook with this ID is already registered (built-in takes priority)
|
|
1229
|
+
const existing = (_registry.get(event) ?? []).find((h) => h.id === hook.id);
|
|
1230
|
+
if (existing) {
|
|
1231
|
+
skipped++;
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
registerHook(event, {
|
|
1236
|
+
id: hook.id,
|
|
1237
|
+
command: hook.command,
|
|
1238
|
+
description: hook.description ?? "",
|
|
1239
|
+
timeout: hook.timeout ?? DEFAULT_TIMEOUT_MS,
|
|
1240
|
+
blocking: hook.blocking ?? false,
|
|
1241
|
+
sdks: hook.sdks ?? [SDK_WILDCARD],
|
|
1242
|
+
builtin: hook.builtin ?? false,
|
|
1243
|
+
retryable: hook.retryable ?? false,
|
|
1244
|
+
maxRetries: hook.maxRetries,
|
|
1245
|
+
env: hook.env,
|
|
1246
|
+
});
|
|
1247
|
+
registered++;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
if (registered > 0) {
|
|
1252
|
+
console.log(`${TAG} registered ${registered} hook(s) from hook library`);
|
|
1253
|
+
}
|
|
1254
|
+
return { registered, skipped };
|
|
1255
|
+
}
|
package/agent/agent-pool.mjs
CHANGED
|
@@ -2871,6 +2871,19 @@ function sdkSupportsPersistentThreads(sdkName) {
|
|
|
2871
2871
|
|
|
2872
2872
|
/** @type {Map<string, ActiveSession>} */
|
|
2873
2873
|
const activeSessions = new Map();
|
|
2874
|
+
const ACTIVE_SESSION_LISTENERS = new Set();
|
|
2875
|
+
|
|
2876
|
+
function notifyActiveSessionListeners(reason = "update", taskKey = null) {
|
|
2877
|
+
if (ACTIVE_SESSION_LISTENERS.size === 0) return;
|
|
2878
|
+
const snapshot = getActiveSessions();
|
|
2879
|
+
for (const listener of ACTIVE_SESSION_LISTENERS) {
|
|
2880
|
+
try {
|
|
2881
|
+
listener(snapshot, { reason, taskKey });
|
|
2882
|
+
} catch {
|
|
2883
|
+
/* best effort */
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2874
2887
|
|
|
2875
2888
|
/** Threshold for approaching-exhaustion warning (80% of MAX_THREAD_TURNS). */
|
|
2876
2889
|
const THREAD_EXHAUSTION_WARNING_THRESHOLD = Math.floor(MAX_THREAD_TURNS * 0.8);
|
|
@@ -2892,6 +2905,7 @@ function registerActiveSession(taskKey, sdk, threadId, sendFn) {
|
|
|
2892
2905
|
send: sendFn,
|
|
2893
2906
|
registeredAt: Date.now(),
|
|
2894
2907
|
});
|
|
2908
|
+
notifyActiveSessionListeners("start", taskKey);
|
|
2895
2909
|
}
|
|
2896
2910
|
|
|
2897
2911
|
/**
|
|
@@ -2900,6 +2914,7 @@ function registerActiveSession(taskKey, sdk, threadId, sendFn) {
|
|
|
2900
2914
|
*/
|
|
2901
2915
|
function unregisterActiveSession(taskKey) {
|
|
2902
2916
|
activeSessions.delete(taskKey);
|
|
2917
|
+
notifyActiveSessionListeners("end", taskKey);
|
|
2903
2918
|
}
|
|
2904
2919
|
|
|
2905
2920
|
/**
|
|
@@ -2949,15 +2964,31 @@ export function hasActiveSession(taskKey) {
|
|
|
2949
2964
|
* Get info about which tasks have active (steerable) sessions.
|
|
2950
2965
|
* @returns {Array<{ taskKey: string, sdk: string, threadId: string|null, age: number }>}
|
|
2951
2966
|
*/
|
|
2967
|
+
export function addActiveSessionListener(listener) {
|
|
2968
|
+
if (typeof listener !== "function") return () => {};
|
|
2969
|
+
ACTIVE_SESSION_LISTENERS.add(listener);
|
|
2970
|
+
return () => ACTIVE_SESSION_LISTENERS.delete(listener);
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2952
2973
|
export function getActiveSessions() {
|
|
2953
2974
|
const now = Date.now();
|
|
2954
2975
|
const result = [];
|
|
2955
2976
|
for (const [key, session] of activeSessions) {
|
|
2977
|
+
const record = threadRegistry.get(key) || null;
|
|
2978
|
+
const createdAt = Number(record?.createdAt || session.registeredAt || now);
|
|
2979
|
+
const lastUsedAt = Number(record?.lastUsedAt || now);
|
|
2956
2980
|
result.push({
|
|
2981
|
+
id: key,
|
|
2982
|
+
taskId: key,
|
|
2957
2983
|
taskKey: key,
|
|
2958
2984
|
sdk: session.sdk,
|
|
2959
2985
|
threadId: session.threadId,
|
|
2960
|
-
|
|
2986
|
+
type: "task",
|
|
2987
|
+
status: "active",
|
|
2988
|
+
turnCount: Number(record?.turnCount || 0),
|
|
2989
|
+
createdAt: new Date(createdAt).toISOString(),
|
|
2990
|
+
lastActiveAt: new Date(lastUsedAt).toISOString(),
|
|
2991
|
+
age: Math.max(0, now - createdAt),
|
|
2961
2992
|
});
|
|
2962
2993
|
}
|
|
2963
2994
|
return result;
|
|
@@ -3973,3 +4004,5 @@ export function getActiveThreads() {
|
|
|
3973
4004
|
}
|
|
3974
4005
|
return result;
|
|
3975
4006
|
}
|
|
4007
|
+
|
|
4008
|
+
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { resolve } from "node:path";
|
|
1
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { resolveRepoRoot } from "../config/repo-root.mjs";
|
|
4
5
|
|
|
5
6
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
@@ -89,6 +90,12 @@ function defaultLogPaths() {
|
|
|
89
90
|
};
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
export function getWeeklyReportStatePath(options = {}) {
|
|
94
|
+
if (options?.statePath) return resolve(String(options.statePath));
|
|
95
|
+
const repoRoot = resolveRepoRoot();
|
|
96
|
+
return resolve(repoRoot, ".cache", "agent-work-logs", "weekly-report-state.json");
|
|
97
|
+
}
|
|
98
|
+
|
|
92
99
|
async function defaultLoadMetrics() {
|
|
93
100
|
const { metricsPath } = defaultLogPaths();
|
|
94
101
|
return await readJsonlFile(metricsPath);
|
|
@@ -99,6 +106,44 @@ async function defaultLoadErrors() {
|
|
|
99
106
|
return await readJsonlFile(errorsPath);
|
|
100
107
|
}
|
|
101
108
|
|
|
109
|
+
export async function readWeeklyReportScheduleState(options = {}) {
|
|
110
|
+
const statePath = getWeeklyReportStatePath(options);
|
|
111
|
+
try {
|
|
112
|
+
const raw = await readFile(statePath, "utf8");
|
|
113
|
+
const parsed = JSON.parse(raw);
|
|
114
|
+
const lastSentAt = String(parsed?.lastSentAt || "").trim();
|
|
115
|
+
if (!lastSentAt) {
|
|
116
|
+
return { statePath, lastSentAt: null };
|
|
117
|
+
}
|
|
118
|
+
const lastSentMs = Date.parse(lastSentAt);
|
|
119
|
+
return {
|
|
120
|
+
statePath,
|
|
121
|
+
lastSentAt: Number.isFinite(lastSentMs) ? new Date(lastSentMs).toISOString() : null,
|
|
122
|
+
};
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (err?.code === "ENOENT") {
|
|
125
|
+
return { statePath, lastSentAt: null };
|
|
126
|
+
}
|
|
127
|
+
return { statePath, lastSentAt: null };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function writeWeeklyReportScheduleState(lastSentAt, options = {}) {
|
|
132
|
+
const statePath = getWeeklyReportStatePath(options);
|
|
133
|
+
const normalized = String(lastSentAt || "").trim();
|
|
134
|
+
const lastSentMs = Date.parse(normalized);
|
|
135
|
+
const payload = {
|
|
136
|
+
lastSentAt: Number.isFinite(lastSentMs) ? new Date(lastSentMs).toISOString() : null,
|
|
137
|
+
updatedAt: new Date().toISOString(),
|
|
138
|
+
};
|
|
139
|
+
await mkdir(dirname(statePath), { recursive: true });
|
|
140
|
+
await writeFile(statePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
141
|
+
return {
|
|
142
|
+
statePath,
|
|
143
|
+
lastSentAt: payload.lastSentAt,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
102
147
|
export function buildWeeklyAgentWorkSummary(options = {}) {
|
|
103
148
|
const now = toSafeDate(options.now);
|
|
104
149
|
const days = toPositiveDays(options.days, DEFAULT_DAYS);
|
|
@@ -359,4 +404,45 @@ export function shouldSendWeeklyReport(options = {}) {
|
|
|
359
404
|
return true;
|
|
360
405
|
}
|
|
361
406
|
return lastSentMs < scheduledThisWeek.getTime();
|
|
362
|
-
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export async function runWeeklyAgentWorkReportCli(options = {}) {
|
|
410
|
+
const argv = Array.isArray(options.argv) ? options.argv : process.argv.slice(2);
|
|
411
|
+
const stdout = options.stdout || process.stdout;
|
|
412
|
+
const stderr = options.stderr || process.stderr;
|
|
413
|
+
const rawDays = String(argv[0] || "").trim();
|
|
414
|
+
const parsedDays = Number.parseInt(rawDays, 10);
|
|
415
|
+
const days = Number.isFinite(parsedDays) && parsedDays > 0 ? parsedDays : undefined;
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
const report = await generateWeeklyAgentWorkReport({
|
|
419
|
+
...(days ? { days } : {}),
|
|
420
|
+
...(options.now ? { now: options.now } : {}),
|
|
421
|
+
...(typeof options.loadMetrics === "function"
|
|
422
|
+
? { loadMetrics: options.loadMetrics }
|
|
423
|
+
: {}),
|
|
424
|
+
...(typeof options.loadErrors === "function"
|
|
425
|
+
? { loadErrors: options.loadErrors }
|
|
426
|
+
: {}),
|
|
427
|
+
});
|
|
428
|
+
stdout.write(`${report.text}\n`);
|
|
429
|
+
if (Array.isArray(report.warnings) && report.warnings.length > 0) {
|
|
430
|
+
stderr.write(`Warnings: ${report.warnings.join(" | ")}\n`);
|
|
431
|
+
}
|
|
432
|
+
return 0;
|
|
433
|
+
} catch (err) {
|
|
434
|
+
stderr.write(`Failed to generate weekly agent work report: ${err?.message || err}\n`);
|
|
435
|
+
return 1;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const isDirectRun =
|
|
440
|
+
process.argv[1] &&
|
|
441
|
+
fileURLToPath(import.meta.url) === resolve(process.argv[1]);
|
|
442
|
+
|
|
443
|
+
if (isDirectRun) {
|
|
444
|
+
const exitCode = await runWeeklyAgentWorkReportCli();
|
|
445
|
+
if (exitCode !== 0) {
|
|
446
|
+
process.exitCode = exitCode;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
@@ -203,8 +203,10 @@ function buildErrorCorrelationEntries(errors, taskProfiles) {
|
|
|
203
203
|
count: 0,
|
|
204
204
|
task_ids: new Set(),
|
|
205
205
|
by_executor: {},
|
|
206
|
+
by_model: {},
|
|
206
207
|
by_size: {},
|
|
207
208
|
by_complexity: {},
|
|
209
|
+
avg_task_duration_ms: 0,
|
|
208
210
|
sample_message: "",
|
|
209
211
|
first_seen_ts: null,
|
|
210
212
|
last_seen_ts: null,
|
|
@@ -240,13 +242,23 @@ function buildErrorCorrelationEntries(errors, taskProfiles) {
|
|
|
240
242
|
extractSizeLabelFromTitle(error.task_title),
|
|
241
243
|
);
|
|
242
244
|
const executor = resolveKnownValue(profile?.executor, error.executor);
|
|
245
|
+
const model = resolveKnownValue(profile?.model, error.model);
|
|
243
246
|
const complexity = resolveKnownValue(profile?.complexity);
|
|
244
247
|
|
|
245
248
|
incrementCounter(entry.by_size, sizeLabel);
|
|
246
249
|
incrementCounter(entry.by_executor, executor);
|
|
250
|
+
incrementCounter(entry.by_model, model);
|
|
247
251
|
incrementCounter(entry.by_complexity, complexity);
|
|
248
252
|
}
|
|
249
253
|
|
|
254
|
+
for (const entry of correlations.values()) {
|
|
255
|
+
const durations = [...entry.task_ids]
|
|
256
|
+
.map((taskId) => taskProfiles.get(taskId)?.avg_duration_ms || 0)
|
|
257
|
+
.filter((duration) => duration > 0);
|
|
258
|
+
entry.avg_task_duration_ms =
|
|
259
|
+
durations.length > 0 ? average(durations) : 0;
|
|
260
|
+
}
|
|
261
|
+
|
|
250
262
|
return correlations;
|
|
251
263
|
}
|
|
252
264
|
|
|
@@ -300,7 +312,9 @@ export function buildErrorCorrelationJsonPayload(summary, { now } = {}) {
|
|
|
300
312
|
last_seen: entry.last_seen_ts
|
|
301
313
|
? new Date(entry.last_seen_ts).toISOString()
|
|
302
314
|
: null,
|
|
315
|
+
avg_task_duration_ms: entry.avg_task_duration_ms || 0,
|
|
303
316
|
by_executor: buildDistribution(entry.by_executor, entry.count),
|
|
317
|
+
by_model: buildDistribution(entry.by_model, entry.count),
|
|
304
318
|
by_size: buildDistribution(entry.by_size, entry.count),
|
|
305
319
|
by_complexity: buildDistribution(entry.by_complexity, entry.count),
|
|
306
320
|
})),
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { readFile, readdir } from "fs/promises";
|
|
22
22
|
import { createReadStream, existsSync } from "fs";
|
|
23
23
|
import { createInterface } from "readline";
|
|
24
|
-
import { resolve, dirname } from "path";
|
|
24
|
+
import { resolve, dirname, isAbsolute } from "path";
|
|
25
25
|
import { fileURLToPath } from "url";
|
|
26
26
|
import {
|
|
27
27
|
buildErrorClusters,
|
|
@@ -44,8 +44,16 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
44
44
|
const __dirname = dirname(__filename);
|
|
45
45
|
const repoRoot = resolve(__dirname, "../..");
|
|
46
46
|
|
|
47
|
+
function resolveAgentWorkLogDir() {
|
|
48
|
+
const configured = String(process.env.AGENT_WORK_LOG_DIR || "").trim();
|
|
49
|
+
if (!configured) {
|
|
50
|
+
return resolve(repoRoot, ".cache/agent-work-logs");
|
|
51
|
+
}
|
|
52
|
+
return isAbsolute(configured) ? configured : resolve(repoRoot, configured);
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
// ── Log Paths ───────────────────────────────────────────────────────────────
|
|
48
|
-
const LOG_DIR =
|
|
56
|
+
const LOG_DIR = resolveAgentWorkLogDir();
|
|
49
57
|
const STREAM_LOG = resolve(LOG_DIR, "agent-work-stream.jsonl");
|
|
50
58
|
const ERRORS_LOG = resolve(LOG_DIR, "agent-errors.jsonl");
|
|
51
59
|
const METRICS_LOG = resolve(LOG_DIR, "agent-metrics.jsonl");
|
|
@@ -339,6 +347,14 @@ function formatDistribution(counts, total, limit = 5) {
|
|
|
339
347
|
.join(", ");
|
|
340
348
|
}
|
|
341
349
|
|
|
350
|
+
function formatDurationMs(durationMs) {
|
|
351
|
+
const value = Number(durationMs) || 0;
|
|
352
|
+
if (value <= 0) return "unknown";
|
|
353
|
+
if (value < 1000) return `${value.toFixed(0)}ms`;
|
|
354
|
+
if (value < 60_000) return `${(value / 1000).toFixed(1)}s`;
|
|
355
|
+
return `${(value / 60_000).toFixed(1)}m`;
|
|
356
|
+
}
|
|
357
|
+
|
|
342
358
|
function topN(obj, n) {
|
|
343
359
|
return Object.entries(obj)
|
|
344
360
|
.sort((a, b) => b[1] - a[1])
|
|
@@ -478,7 +494,7 @@ async function correlateErrors(options) {
|
|
|
478
494
|
const errors = await loadErrors({ days: windowDays });
|
|
479
495
|
|
|
480
496
|
if (errors.length === 0) {
|
|
481
|
-
const message = "No
|
|
497
|
+
const message = "No data found for selected window";
|
|
482
498
|
if (useJson) {
|
|
483
499
|
console.log(
|
|
484
500
|
JSON.stringify(
|
|
@@ -522,10 +538,14 @@ async function correlateErrors(options) {
|
|
|
522
538
|
console.log(
|
|
523
539
|
` Executors: ${formatDistribution(entry.by_executor, entry.count)}`,
|
|
524
540
|
);
|
|
541
|
+
console.log(` Models: ${formatDistribution(entry.by_model, entry.count)}`);
|
|
525
542
|
console.log(` Sizes: ${formatDistribution(entry.by_size, entry.count)}`);
|
|
526
543
|
console.log(
|
|
527
544
|
` Complexity: ${formatDistribution(entry.by_complexity, entry.count)}`,
|
|
528
545
|
);
|
|
546
|
+
console.log(
|
|
547
|
+
` Avg task duration: ${formatDurationMs(entry.avg_task_duration_ms)}`,
|
|
548
|
+
);
|
|
529
549
|
if (entry.sample_message) {
|
|
530
550
|
console.log(
|
|
531
551
|
` Sample: ${entry.sample_message.slice(0, 100)}${entry.sample_message.length > 100 ? "..." : ""}`,
|