bosun 0.41.0 → 0.41.2
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 +8 -0
- package/README.md +20 -0
- package/agent/agent-event-bus.mjs +248 -6
- package/agent/agent-pool.mjs +125 -28
- package/agent/agent-work-analyzer.mjs +8 -16
- package/agent/retry-queue.mjs +164 -0
- package/bosun.config.example.json +25 -0
- package/bosun.schema.json +825 -183
- package/cli.mjs +59 -5
- package/config/config.mjs +130 -3
- package/infra/monitor.mjs +693 -67
- package/infra/runtime-accumulator.mjs +376 -84
- package/infra/session-tracker.mjs +82 -25
- package/lib/codebase-audit.mjs +133 -18
- package/package.json +23 -4
- package/server/setup-web-server.mjs +25 -0
- package/server/ui-server.mjs +248 -29
- package/setup.mjs +27 -24
- package/shell/codex-shell.mjs +34 -3
- package/shell/copilot-shell.mjs +50 -8
- package/task/msg-hub.mjs +193 -0
- package/task/pipeline.mjs +544 -0
- package/task/task-cli.mjs +38 -2
- package/task/task-executor-pipeline.mjs +143 -0
- package/task/task-executor.mjs +36 -27
- package/telegram/get-telegram-chat-id.mjs +57 -47
- package/ui/components/workspace-switcher.js +7 -7
- package/ui/demo-defaults.js +15694 -10573
- package/ui/modules/settings-schema.js +2 -0
- package/ui/modules/state.js +54 -57
- package/ui/modules/voice-client-sdk.js +375 -36
- package/ui/modules/voice-client.js +140 -31
- package/ui/setup.html +68 -2
- package/ui/styles/components.css +57 -0
- package/ui/styles.css +201 -1
- package/ui/tabs/dashboard.js +74 -0
- package/ui/tabs/logs.js +10 -0
- package/ui/tabs/settings.js +178 -99
- package/ui/tabs/tasks.js +31 -1
- package/ui/tabs/telemetry.js +34 -0
- package/ui/tabs/workflow-canvas-utils.mjs +8 -1
- package/ui/tabs/workflows.js +532 -275
- package/voice/voice-agents-sdk.mjs +1 -1
- package/voice/voice-relay.mjs +6 -6
- package/workflow/declarative-workflows.mjs +145 -0
- package/workflow/msg-hub.mjs +237 -0
- package/workflow/pipeline-workflows.mjs +287 -0
- package/workflow/pipeline.mjs +828 -315
- package/workflow/workflow-cli.mjs +128 -0
- package/workflow/workflow-engine.mjs +329 -17
- package/workflow/workflow-nodes/custom-loader.mjs +250 -0
- package/workflow/workflow-nodes.mjs +1955 -223
- package/workflow/workflow-templates.mjs +26 -8
- package/workflow-templates/agents.mjs +0 -1
- package/workflow-templates/bosun-native.mjs +212 -2
- package/workflow-templates/continuation-loop.mjs +339 -0
- package/workflow-templates/github.mjs +516 -40
- package/workflow-templates/planning.mjs +446 -17
- package/workflow-templates/reliability.mjs +65 -12
- package/workflow-templates/task-batch.mjs +24 -8
- package/workflow-templates/task-lifecycle.mjs +83 -6
- package/workspace/context-cache.mjs +66 -18
- package/workspace/workspace-manager.mjs +2 -1
- package/workflow-templates/issue-continuation.mjs +0 -243
package/ui/tabs/dashboard.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
projectSummary,
|
|
23
23
|
loadStatus,
|
|
24
24
|
loadProjectSummary,
|
|
25
|
+
loadRetryQueue,
|
|
25
26
|
showToast,
|
|
26
27
|
refreshTab,
|
|
27
28
|
runOptimistic,
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
getDashboardHistory,
|
|
31
32
|
setPendingChange,
|
|
32
33
|
clearPendingChange,
|
|
34
|
+
retryQueueData,
|
|
33
35
|
} from "../modules/state.js";
|
|
34
36
|
import { navigateTo } from "../modules/router.js";
|
|
35
37
|
import { ICONS } from "../modules/icons.js";
|
|
@@ -322,6 +324,7 @@ export function DashboardTab() {
|
|
|
322
324
|
const prevCounts = useRef(null);
|
|
323
325
|
const status = statusData.value;
|
|
324
326
|
const executor = executorData.value;
|
|
327
|
+
const retryQueue = retryQueueData.value || { count: 0, items: [], stats: {} };
|
|
325
328
|
const project = projectSummary.value;
|
|
326
329
|
const counts = status?.counts || {};
|
|
327
330
|
const summary = status?.success_metrics || {};
|
|
@@ -448,6 +451,10 @@ export function DashboardTab() {
|
|
|
448
451
|
.catch(() => {});
|
|
449
452
|
}, []);
|
|
450
453
|
|
|
454
|
+
useEffect(() => {
|
|
455
|
+
loadRetryQueue().catch(() => {});
|
|
456
|
+
}, []);
|
|
457
|
+
|
|
451
458
|
// ── Flash metrics on counts change ──
|
|
452
459
|
useEffect(() => {
|
|
453
460
|
const current = JSON.stringify(counts);
|
|
@@ -657,6 +664,34 @@ export function DashboardTab() {
|
|
|
657
664
|
|
|
658
665
|
/* ── Recent activity (last 5 tasks from global tasks signal) ── */
|
|
659
666
|
const recentTasks = (tasksData.value || []).slice(0, 5);
|
|
667
|
+
const retryItems = Array.isArray(retryQueue.items) ? retryQueue.items : [];
|
|
668
|
+
|
|
669
|
+
const formatRetryCountdown = useCallback((nextAttemptAt) => {
|
|
670
|
+
const target = Number(nextAttemptAt || 0);
|
|
671
|
+
if (!Number.isFinite(target) || target <= 0) return "Now";
|
|
672
|
+
const remainingMs = target - now.getTime();
|
|
673
|
+
if (remainingMs <= 0) return "Now";
|
|
674
|
+
const totalSec = Math.ceil(remainingMs / 1000);
|
|
675
|
+
const min = Math.floor(totalSec / 60);
|
|
676
|
+
const sec = totalSec % 60;
|
|
677
|
+
if (min > 0) return `${min}m ${String(sec).padStart(2, "0")}s`;
|
|
678
|
+
return `${sec}s`;
|
|
679
|
+
}, [now]);
|
|
680
|
+
|
|
681
|
+
const handleRetryNow = useCallback(async (taskId) => {
|
|
682
|
+
const id = String(taskId || "").trim();
|
|
683
|
+
if (!id) return;
|
|
684
|
+
try {
|
|
685
|
+
await apiFetch("/api/tasks/retry", {
|
|
686
|
+
method: "POST",
|
|
687
|
+
body: JSON.stringify({ taskId: id }),
|
|
688
|
+
});
|
|
689
|
+
showToast(`Retry requested for ${id}`, "success");
|
|
690
|
+
scheduleRefresh(100);
|
|
691
|
+
} catch {
|
|
692
|
+
/* toast shown by apiFetch */
|
|
693
|
+
}
|
|
694
|
+
}, []);
|
|
660
695
|
|
|
661
696
|
/* ── Loading skeleton ── */
|
|
662
697
|
if (!status && !executor)
|
|
@@ -981,6 +1016,45 @@ export function DashboardTab() {
|
|
|
981
1016
|
</div>
|
|
982
1017
|
<//>
|
|
983
1018
|
|
|
1019
|
+
<${Card}
|
|
1020
|
+
title=${html`<span class="dashboard-card-title"
|
|
1021
|
+
><span class="dashboard-title-icon">${ICONS.refresh || resolveIcon("refresh")}</span>Retry Queue</span
|
|
1022
|
+
>`}
|
|
1023
|
+
className="dashboard-card dashboard-retry-queue"
|
|
1024
|
+
>
|
|
1025
|
+
${retryItems.length
|
|
1026
|
+
? html`
|
|
1027
|
+
<div class="dashboard-retry-list">
|
|
1028
|
+
${retryItems.map((item) => html`
|
|
1029
|
+
<div key=${item.taskId} class="dashboard-retry-item">
|
|
1030
|
+
<div class="dashboard-retry-main">
|
|
1031
|
+
<div class="dashboard-retry-task">${item.taskId}</div>
|
|
1032
|
+
${item.taskTitle
|
|
1033
|
+
? html`<div class="dashboard-retry-task-title">${truncate(item.taskTitle, 72)}</div>`
|
|
1034
|
+
: null}
|
|
1035
|
+
<div class="dashboard-retry-error">${truncate(item.lastError || item.reason || "Unknown retry reason", 120)}</div>
|
|
1036
|
+
</div>
|
|
1037
|
+
<div class="dashboard-retry-meta">
|
|
1038
|
+
<span class="dashboard-retry-pill">Attempt ${item.retryCount || 0}</span>
|
|
1039
|
+
<span class="dashboard-retry-pill">Next ${formatRetryCountdown(item.nextAttemptAt)}</span>
|
|
1040
|
+
<${Button}
|
|
1041
|
+
variant="outlined"
|
|
1042
|
+
size="small"
|
|
1043
|
+
onClick=${() => {
|
|
1044
|
+
haptic("medium");
|
|
1045
|
+
void handleRetryNow(item.taskId);
|
|
1046
|
+
}}
|
|
1047
|
+
>
|
|
1048
|
+
Retry Now
|
|
1049
|
+
<//>
|
|
1050
|
+
</div>
|
|
1051
|
+
</div>
|
|
1052
|
+
`)}
|
|
1053
|
+
</div>
|
|
1054
|
+
`
|
|
1055
|
+
: html`<${EmptyState} title="No queued retries" description="Failed tasks waiting to retry appear here automatically." />`}
|
|
1056
|
+
<//>
|
|
1057
|
+
|
|
984
1058
|
<${Card}
|
|
985
1059
|
title=${html`<span class="dashboard-card-title"
|
|
986
1060
|
><span class="dashboard-title-icon">${ICONS.clock}</span>Recent
|
package/ui/tabs/logs.js
CHANGED
|
@@ -237,6 +237,16 @@ export function LogsTab() {
|
|
|
237
237
|
loadLogs();
|
|
238
238
|
}, [isMobile]);
|
|
239
239
|
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
const timer = setInterval(() => {
|
|
242
|
+
loadLogs({ force: true });
|
|
243
|
+
if (agentLogFile?.value) {
|
|
244
|
+
loadAgentLogTailData({ force: true });
|
|
245
|
+
}
|
|
246
|
+
}, 3000);
|
|
247
|
+
return () => clearInterval(timer);
|
|
248
|
+
}, []);
|
|
249
|
+
|
|
240
250
|
const branchFileDetails = useMemo(() => {
|
|
241
251
|
if (!branchDetail) return [];
|
|
242
252
|
if (Array.isArray(branchDetail.filesChanged) && branchDetail.filesChanged.length) {
|
package/ui/tabs/settings.js
CHANGED
|
@@ -718,6 +718,78 @@ function maskValue(val) {
|
|
|
718
718
|
return "••••••" + s.slice(-4);
|
|
719
719
|
}
|
|
720
720
|
|
|
721
|
+
const EXECUTOR_SECTION_ORDER = [
|
|
722
|
+
"Runtime & Pool",
|
|
723
|
+
"Routing & Planning",
|
|
724
|
+
"SDK Availability",
|
|
725
|
+
"Provider Credentials",
|
|
726
|
+
"Codex Models",
|
|
727
|
+
"Claude Models",
|
|
728
|
+
"Gemini Models",
|
|
729
|
+
"Other",
|
|
730
|
+
];
|
|
731
|
+
|
|
732
|
+
const EXECUTOR_SECTION_DESCRIPTIONS = {
|
|
733
|
+
"Runtime & Pool": "Core runtime behavior for the internal executor pool, including parallelism, SDK selection, and timeouts.",
|
|
734
|
+
"Routing & Planning": "How Bosun distributes tasks, chooses fallback executors, and shapes planning behavior.",
|
|
735
|
+
"SDK Availability": "Enable or disable SDK families from the runtime picker and task execution pool.",
|
|
736
|
+
"Provider Credentials": "API credentials used by executor backends.",
|
|
737
|
+
"Codex Models": "Codex-specific model, profile, and subagent settings.",
|
|
738
|
+
"Claude Models": "Claude-specific model settings.",
|
|
739
|
+
"Gemini Models": "Gemini-specific model settings.",
|
|
740
|
+
"Other": "Additional executor settings.",
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
function getExecutorSection(def) {
|
|
744
|
+
const key = String(def?.key || "");
|
|
745
|
+
if (!key) return "Other";
|
|
746
|
+
if ([
|
|
747
|
+
"EXECUTOR_MODE",
|
|
748
|
+
"INTERNAL_EXECUTOR_PARALLEL",
|
|
749
|
+
"INTERNAL_EXECUTOR_SDK",
|
|
750
|
+
"INTERNAL_EXECUTOR_TIMEOUT_MS",
|
|
751
|
+
"INTERNAL_EXECUTOR_MAX_RETRIES",
|
|
752
|
+
"INTERNAL_EXECUTOR_POLL_MS",
|
|
753
|
+
"PRIMARY_AGENT",
|
|
754
|
+
].includes(key)) return "Runtime & Pool";
|
|
755
|
+
if ([
|
|
756
|
+
"INTERNAL_EXECUTOR_REVIEW_AGENT_ENABLED",
|
|
757
|
+
"INTERNAL_EXECUTOR_REPLENISH_ENABLED",
|
|
758
|
+
"EXECUTORS",
|
|
759
|
+
"EXECUTOR_DISTRIBUTION",
|
|
760
|
+
"FAILOVER_STRATEGY",
|
|
761
|
+
"COMPLEXITY_ROUTING_ENABLED",
|
|
762
|
+
"PROJECT_REQUIREMENTS_PROFILE",
|
|
763
|
+
].includes(key)) return "Routing & Planning";
|
|
764
|
+
if (key.endsWith("_SDK_DISABLED")) return "SDK Availability";
|
|
765
|
+
if (key.endsWith("API_KEY")) return "Provider Credentials";
|
|
766
|
+
if (key.startsWith("CODEX_")) return "Codex Models";
|
|
767
|
+
if (key.startsWith("CLAUDE_")) return "Claude Models";
|
|
768
|
+
if (key.startsWith("GEMINI_") || key.startsWith("GOOGLE_")) return "Gemini Models";
|
|
769
|
+
return "Other";
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function groupExecutorSettings(defs = []) {
|
|
773
|
+
const groups = new Map();
|
|
774
|
+
for (const def of defs) {
|
|
775
|
+
const section = getExecutorSection(def);
|
|
776
|
+
if (!groups.has(section)) groups.set(section, []);
|
|
777
|
+
groups.get(section).push(def);
|
|
778
|
+
}
|
|
779
|
+
return EXECUTOR_SECTION_ORDER
|
|
780
|
+
.filter((section) => groups.has(section))
|
|
781
|
+
.map((section) => ({
|
|
782
|
+
title: section,
|
|
783
|
+
description: EXECUTOR_SECTION_DESCRIPTIONS[section] || "",
|
|
784
|
+
defs: groups.get(section),
|
|
785
|
+
}));
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function formatCountdownSeconds(ms) {
|
|
789
|
+
const remaining = Math.max(0, Number(ms) || 0);
|
|
790
|
+
return Math.ceil(remaining / 1000);
|
|
791
|
+
}
|
|
792
|
+
|
|
721
793
|
/* ═══════════════════════════════════════════════════════════════
|
|
722
794
|
* ServerConfigMode — .env management UI
|
|
723
795
|
* ═══════════════════════════════════════════════════════════════ */
|
|
@@ -750,8 +822,10 @@ function ServerConfigMode() {
|
|
|
750
822
|
/* Save flow */
|
|
751
823
|
const [saving, setSaving] = useState(false);
|
|
752
824
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
825
|
+
const [restartCountdown, setRestartCountdown] = useState(null);
|
|
753
826
|
|
|
754
827
|
const tooltipTimer = useRef(null);
|
|
828
|
+
const restartCountdownTimer = useRef(null);
|
|
755
829
|
|
|
756
830
|
/* ─── Load server settings on mount ─── */
|
|
757
831
|
const fetchSettings = useCallback(async (opts = {}) => {
|
|
@@ -801,6 +875,16 @@ function ServerConfigMode() {
|
|
|
801
875
|
|
|
802
876
|
useEffect(() => { fetchSettings(); }, [fetchSettings]);
|
|
803
877
|
|
|
878
|
+
const clearRestartCountdown = useCallback(() => {
|
|
879
|
+
if (restartCountdownTimer.current) {
|
|
880
|
+
clearInterval(restartCountdownTimer.current);
|
|
881
|
+
restartCountdownTimer.current = null;
|
|
882
|
+
}
|
|
883
|
+
setRestartCountdown(null);
|
|
884
|
+
}, []);
|
|
885
|
+
|
|
886
|
+
useEffect(() => () => clearRestartCountdown(), [clearRestartCountdown]);
|
|
887
|
+
|
|
804
888
|
/* ─── Grouped settings with search + advanced filter ─── */
|
|
805
889
|
const grouped = useMemo(() => getGroupedSettings(showAdvanced), [showAdvanced]);
|
|
806
890
|
const isContextShreddingSetting = useCallback((def) => {
|
|
@@ -830,6 +914,15 @@ function ServerConfigMode() {
|
|
|
830
914
|
[edits, serverData],
|
|
831
915
|
);
|
|
832
916
|
|
|
917
|
+
const getReloadDelayMs = useCallback(
|
|
918
|
+
(changes = null) => {
|
|
919
|
+
const raw = changes?.ENV_RELOAD_DELAY_MS ?? getValue("ENV_RELOAD_DELAY_MS") ?? "";
|
|
920
|
+
const parsed = Number.parseInt(String(raw || ""), 10);
|
|
921
|
+
return Number.isFinite(parsed) ? Math.max(500, parsed) : 5000;
|
|
922
|
+
},
|
|
923
|
+
[getValue],
|
|
924
|
+
);
|
|
925
|
+
|
|
833
926
|
/* ─── Determine if a value matches its default ─── */
|
|
834
927
|
const isDefault = useCallback(
|
|
835
928
|
(def) => {
|
|
@@ -881,6 +974,22 @@ function ServerConfigMode() {
|
|
|
881
974
|
});
|
|
882
975
|
}, [edits]);
|
|
883
976
|
|
|
977
|
+
const restartCountdownSeconds = restartCountdown
|
|
978
|
+
? formatCountdownSeconds(restartCountdown.remainingMs)
|
|
979
|
+
: null;
|
|
980
|
+
|
|
981
|
+
useEffect(() => {
|
|
982
|
+
if (!restartCountdown) return undefined;
|
|
983
|
+
if (restartCountdown.remainingMs > 0 || !wsConnected.value) return undefined;
|
|
984
|
+
const timer = setTimeout(() => {
|
|
985
|
+
setRestartCountdown((current) => {
|
|
986
|
+
if (!current || current.remainingMs > 0) return current;
|
|
987
|
+
return null;
|
|
988
|
+
});
|
|
989
|
+
}, 3000);
|
|
990
|
+
return () => clearTimeout(timer);
|
|
991
|
+
}, [restartCountdown, wsConnected.value]);
|
|
992
|
+
|
|
884
993
|
/* ─── Handlers ─── */
|
|
885
994
|
const handleChange = useCallback(
|
|
886
995
|
(key, value) => {
|
|
@@ -959,6 +1068,7 @@ function ServerConfigMode() {
|
|
|
959
1068
|
changes[key] = value;
|
|
960
1069
|
}
|
|
961
1070
|
const changeKeys = Object.keys(changes);
|
|
1071
|
+
const restartDelayMs = getReloadDelayMs(changes);
|
|
962
1072
|
if (changeKeys.length > 0) {
|
|
963
1073
|
let res;
|
|
964
1074
|
try {
|
|
@@ -1017,7 +1127,29 @@ function ServerConfigMode() {
|
|
|
1017
1127
|
showToast("Settings saved successfully", "success");
|
|
1018
1128
|
haptic("medium");
|
|
1019
1129
|
if (hasRestartSetting && changeKeys.length > 0) {
|
|
1020
|
-
|
|
1130
|
+
if (restartCountdownTimer.current) {
|
|
1131
|
+
clearInterval(restartCountdownTimer.current);
|
|
1132
|
+
}
|
|
1133
|
+
setRestartCountdown({
|
|
1134
|
+
remainingMs: restartDelayMs,
|
|
1135
|
+
totalMs: restartDelayMs,
|
|
1136
|
+
keys: changeKeys.filter((key) => {
|
|
1137
|
+
const def = SETTINGS_SCHEMA.find((entry) => entry.key === key);
|
|
1138
|
+
return def?.restart;
|
|
1139
|
+
}),
|
|
1140
|
+
});
|
|
1141
|
+
restartCountdownTimer.current = setInterval(() => {
|
|
1142
|
+
setRestartCountdown((current) => {
|
|
1143
|
+
if (!current) return current;
|
|
1144
|
+
const nextRemaining = Math.max(0, current.remainingMs - 1000);
|
|
1145
|
+
if (nextRemaining <= 0 && restartCountdownTimer.current) {
|
|
1146
|
+
clearInterval(restartCountdownTimer.current);
|
|
1147
|
+
restartCountdownTimer.current = null;
|
|
1148
|
+
}
|
|
1149
|
+
return { ...current, remainingMs: nextRemaining };
|
|
1150
|
+
});
|
|
1151
|
+
}, 1000);
|
|
1152
|
+
showToast(`Restart-sensitive settings saved. Reload countdown started (${formatCountdownSeconds(restartDelayMs)}s).`, "info");
|
|
1021
1153
|
}
|
|
1022
1154
|
} catch (err) {
|
|
1023
1155
|
let parsed = null;
|
|
@@ -1035,7 +1167,7 @@ function ServerConfigMode() {
|
|
|
1035
1167
|
} finally {
|
|
1036
1168
|
setSaving(false);
|
|
1037
1169
|
}
|
|
1038
|
-
}, [edits, hasRestartSetting, serverMeta, fetchSettings]);
|
|
1170
|
+
}, [edits, hasRestartSetting, serverMeta, fetchSettings, getReloadDelayMs]);
|
|
1039
1171
|
|
|
1040
1172
|
const handleCancelSave = useCallback(() => {
|
|
1041
1173
|
setConfirmOpen(false);
|
|
@@ -1341,6 +1473,26 @@ function ServerConfigMode() {
|
|
|
1341
1473
|
</div>
|
|
1342
1474
|
`}
|
|
1343
1475
|
|
|
1476
|
+
${restartCountdown &&
|
|
1477
|
+
html`
|
|
1478
|
+
<div class="settings-banner ${restartCountdownSeconds <= 2 ? "settings-banner-warn" : "settings-banner-info"}">
|
|
1479
|
+
<span>${resolveIcon(":refresh:")}</span>
|
|
1480
|
+
<span class="settings-banner-text">
|
|
1481
|
+
<strong>
|
|
1482
|
+
${restartCountdownSeconds > 0
|
|
1483
|
+
? `Reload scheduled in ${restartCountdownSeconds}s`
|
|
1484
|
+
: wsOk
|
|
1485
|
+
? "Reload window elapsed"
|
|
1486
|
+
: "Reloading now"}
|
|
1487
|
+
</strong>
|
|
1488
|
+
${restartCountdown.keys?.length
|
|
1489
|
+
? ` — Applying restart-sensitive changes: ${restartCountdown.keys.slice(0, 3).join(", ")}${restartCountdown.keys.length > 3 ? ` +${restartCountdown.keys.length - 3} more` : ""}.`
|
|
1490
|
+
: " — Applying restart-sensitive configuration updates."}
|
|
1491
|
+
${!wsOk ? " Connection may drop briefly while Bosun restarts." : " Connection may briefly reset while Bosun reloads."}
|
|
1492
|
+
</span>
|
|
1493
|
+
</div>
|
|
1494
|
+
`}
|
|
1495
|
+
|
|
1344
1496
|
<!-- Search bar -->
|
|
1345
1497
|
<div class="settings-search">
|
|
1346
1498
|
<${SearchInput}
|
|
@@ -1469,11 +1621,19 @@ function ServerConfigMode() {
|
|
|
1469
1621
|
</div>
|
|
1470
1622
|
<//>
|
|
1471
1623
|
`
|
|
1472
|
-
:
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1624
|
+
: activeCategory === "executor"
|
|
1625
|
+
? groupExecutorSettings(catDefs).map((section) => html`
|
|
1626
|
+
<${Card} key=${section.title}>
|
|
1627
|
+
<div class="card-subtitle mb-sm" style="font-size:13px;font-weight:700">${section.title}</div>
|
|
1628
|
+
${section.description && html`<div class="meta-text mb-sm">${section.description}</div>`}
|
|
1629
|
+
${section.defs.map((def) => renderSetting(def))}
|
|
1630
|
+
<//>
|
|
1631
|
+
`)
|
|
1632
|
+
: html`
|
|
1633
|
+
<${Card}>
|
|
1634
|
+
${catDefs.map((def) => renderSetting(def))}
|
|
1635
|
+
<//>
|
|
1636
|
+
`}
|
|
1477
1637
|
`;
|
|
1478
1638
|
})()}
|
|
1479
1639
|
|
|
@@ -1493,7 +1653,15 @@ function ServerConfigMode() {
|
|
|
1493
1653
|
<div class=${`settings-save-bar ${changeCount > 0 ? 'settings-save-bar--dirty' : 'settings-save-bar--clean'}`}>
|
|
1494
1654
|
<div class="save-bar-info">
|
|
1495
1655
|
<span class=${`setting-modified-dot ${changeCount === 0 ? 'setting-modified-dot--clean' : ''}`}></span>
|
|
1496
|
-
<span
|
|
1656
|
+
<span>
|
|
1657
|
+
${changeCount > 0
|
|
1658
|
+
? `${changeCount} unsaved change${changeCount !== 1 ? "s" : ""}`
|
|
1659
|
+
: restartCountdownSeconds != null
|
|
1660
|
+
? restartCountdownSeconds > 0
|
|
1661
|
+
? `Reload scheduled in ${restartCountdownSeconds}s`
|
|
1662
|
+
: (wsOk ? "Waiting for runtime reload" : "Restarting now")
|
|
1663
|
+
: "All changes saved"}
|
|
1664
|
+
</span>
|
|
1497
1665
|
</div>
|
|
1498
1666
|
<div class="save-bar-actions">
|
|
1499
1667
|
${changeCount > 0 && html`
|
|
@@ -1537,7 +1705,7 @@ function ServerConfigMode() {
|
|
|
1537
1705
|
<div class="settings-banner settings-banner-warn" style="margin-top:8px">
|
|
1538
1706
|
<span>${resolveIcon(":refresh:")}</span>
|
|
1539
1707
|
<span class="settings-banner-text">
|
|
1540
|
-
Some changes require a restart.
|
|
1708
|
+
Some changes require a restart. Bosun will begin reloading after about ${formatCountdownSeconds(getReloadDelayMs(edits))}s, and the countdown will stay visible after save.
|
|
1541
1709
|
</span>
|
|
1542
1710
|
</div>
|
|
1543
1711
|
`}
|
|
@@ -1623,9 +1791,6 @@ function AppPreferencesMode() {
|
|
|
1623
1791
|
const [notifyErrors, setNotifyErrors] = useState(true);
|
|
1624
1792
|
const [notifyComplete, setNotifyComplete] = useState(true);
|
|
1625
1793
|
const [debugMode, setDebugMode] = useState(false);
|
|
1626
|
-
const [defaultMaxParallel, setDefaultMaxParallel] = useState(4);
|
|
1627
|
-
const [defaultSdk, setDefaultSdk] = useState("auto");
|
|
1628
|
-
const [defaultRegion, setDefaultRegion] = useState("auto");
|
|
1629
1794
|
const [showRawJson, setShowRawJson] = useState(false);
|
|
1630
1795
|
const [loaded, setLoaded] = useState(false);
|
|
1631
1796
|
|
|
@@ -1683,16 +1848,13 @@ function AppPreferencesMode() {
|
|
|
1683
1848
|
useEffect(() => {
|
|
1684
1849
|
(async () => {
|
|
1685
1850
|
try {
|
|
1686
|
-
const [fs, ct, nu, ne, nc, dm
|
|
1851
|
+
const [fs, ct, nu, ne, nc, dm] = await Promise.all([
|
|
1687
1852
|
cloudGet("fontSize"),
|
|
1688
1853
|
cloudGet("colorTheme"),
|
|
1689
1854
|
cloudGet("notifyUpdates"),
|
|
1690
1855
|
cloudGet("notifyErrors"),
|
|
1691
1856
|
cloudGet("notifyComplete"),
|
|
1692
1857
|
cloudGet("debugMode"),
|
|
1693
|
-
cloudGet("defaultMaxParallel"),
|
|
1694
|
-
cloudGet("defaultSdk"),
|
|
1695
|
-
cloudGet("defaultRegion"),
|
|
1696
1858
|
]);
|
|
1697
1859
|
if (fs) {
|
|
1698
1860
|
setFontSize(fs);
|
|
@@ -1706,9 +1868,6 @@ function AppPreferencesMode() {
|
|
|
1706
1868
|
if (ne != null) setNotifyErrors(ne);
|
|
1707
1869
|
if (nc != null) setNotifyComplete(nc);
|
|
1708
1870
|
if (dm != null) setDebugMode(dm);
|
|
1709
|
-
if (dmp != null) setDefaultMaxParallel(dmp);
|
|
1710
|
-
if (ds) setDefaultSdk(ds);
|
|
1711
|
-
if (dr) setDefaultRegion(dr);
|
|
1712
1871
|
} catch (err) {
|
|
1713
1872
|
console.warn('[AppPrefs] Failed to load preferences:', err);
|
|
1714
1873
|
} finally {
|
|
@@ -1745,31 +1904,6 @@ function AppPreferencesMode() {
|
|
|
1745
1904
|
showToast("Theme saved", "success");
|
|
1746
1905
|
};
|
|
1747
1906
|
|
|
1748
|
-
const handleDefaultMaxParallel = (v) => {
|
|
1749
|
-
const val = Math.max(1, Math.min(20, Number(v)));
|
|
1750
|
-
setDefaultMaxParallel(val);
|
|
1751
|
-
cloudSet("defaultMaxParallel", val);
|
|
1752
|
-
console.log('[AppPrefs] Saved: defaultMaxParallel', val);
|
|
1753
|
-
haptic();
|
|
1754
|
-
showToast("Preference saved", "success");
|
|
1755
|
-
};
|
|
1756
|
-
|
|
1757
|
-
const handleDefaultSdk = (v) => {
|
|
1758
|
-
setDefaultSdk(v);
|
|
1759
|
-
cloudSet("defaultSdk", v);
|
|
1760
|
-
console.log('[AppPrefs] Saved: defaultSdk', v);
|
|
1761
|
-
haptic();
|
|
1762
|
-
showToast("Preference saved", "success");
|
|
1763
|
-
};
|
|
1764
|
-
|
|
1765
|
-
const handleDefaultRegion = (v) => {
|
|
1766
|
-
setDefaultRegion(v);
|
|
1767
|
-
cloudSet("defaultRegion", v);
|
|
1768
|
-
console.log('[AppPrefs] Saved: defaultRegion', v);
|
|
1769
|
-
haptic();
|
|
1770
|
-
showToast("Preference saved", "success");
|
|
1771
|
-
};
|
|
1772
|
-
|
|
1773
1907
|
/* Clear cache */
|
|
1774
1908
|
const handleClearCache = async () => {
|
|
1775
1909
|
const ok = await showConfirm("Clear all cached data and preferences?");
|
|
@@ -1782,9 +1916,6 @@ function AppPreferencesMode() {
|
|
|
1782
1916
|
"notifyErrors",
|
|
1783
1917
|
"notifyComplete",
|
|
1784
1918
|
"debugMode",
|
|
1785
|
-
"defaultMaxParallel",
|
|
1786
|
-
"defaultSdk",
|
|
1787
|
-
"defaultRegion",
|
|
1788
1919
|
];
|
|
1789
1920
|
for (const k of keys) cloudRemove(k);
|
|
1790
1921
|
showToast("Cache cleared — reload to apply", "success");
|
|
@@ -1802,9 +1933,6 @@ function AppPreferencesMode() {
|
|
|
1802
1933
|
"notifyErrors",
|
|
1803
1934
|
"notifyComplete",
|
|
1804
1935
|
"debugMode",
|
|
1805
|
-
"defaultMaxParallel",
|
|
1806
|
-
"defaultSdk",
|
|
1807
|
-
"defaultRegion",
|
|
1808
1936
|
];
|
|
1809
1937
|
for (const k of keys) cloudRemove(k);
|
|
1810
1938
|
setFontSize("medium");
|
|
@@ -1812,9 +1940,6 @@ function AppPreferencesMode() {
|
|
|
1812
1940
|
setNotifyErrors(true);
|
|
1813
1941
|
setNotifyComplete(true);
|
|
1814
1942
|
setDebugMode(false);
|
|
1815
|
-
setDefaultMaxParallel(4);
|
|
1816
|
-
setDefaultSdk("auto");
|
|
1817
|
-
setDefaultRegion("auto");
|
|
1818
1943
|
setColorTheme("system");
|
|
1819
1944
|
document.documentElement.removeAttribute("data-theme");
|
|
1820
1945
|
document.documentElement.setAttribute(THEME_LOCK_ATTR, "system");
|
|
@@ -1982,52 +2107,6 @@ function AppPreferencesMode() {
|
|
|
1982
2107
|
<//>
|
|
1983
2108
|
<//>
|
|
1984
2109
|
|
|
1985
|
-
<!-- ─── Executor Defaults ─── -->
|
|
1986
|
-
<${Collapsible} title=${iconText(":settings: Executor Defaults")} defaultOpen=${false}>
|
|
1987
|
-
<${Card}>
|
|
1988
|
-
<div class="card-subtitle mb-sm">Default Max Parallel</div>
|
|
1989
|
-
<div class="range-row mb-md">
|
|
1990
|
-
<${Slider}
|
|
1991
|
-
min=${1}
|
|
1992
|
-
max=${20}
|
|
1993
|
-
step=${1}
|
|
1994
|
-
value=${defaultMaxParallel}
|
|
1995
|
-
onChange=${(e, v) => setDefaultMaxParallel(v)}
|
|
1996
|
-
onChangeCommitted=${(e, v) => handleDefaultMaxParallel(v)}
|
|
1997
|
-
/>
|
|
1998
|
-
<span class="pill">${defaultMaxParallel}</span>
|
|
1999
|
-
</div>
|
|
2000
|
-
|
|
2001
|
-
<div class="card-subtitle mb-sm">Default SDK</div>
|
|
2002
|
-
<${SegmentedControl}
|
|
2003
|
-
options=${[
|
|
2004
|
-
{ value: "codex", label: "Codex" },
|
|
2005
|
-
{ value: "copilot", label: "Copilot" },
|
|
2006
|
-
{ value: "claude", label: "Claude" },
|
|
2007
|
-
{ value: "auto", label: "Auto" },
|
|
2008
|
-
]}
|
|
2009
|
-
value=${defaultSdk}
|
|
2010
|
-
onChange=${handleDefaultSdk}
|
|
2011
|
-
/>
|
|
2012
|
-
|
|
2013
|
-
<div class="card-subtitle mt-md mb-sm">Default Region</div>
|
|
2014
|
-
${(() => {
|
|
2015
|
-
const regions = configData.value?.regions || ["auto"];
|
|
2016
|
-
const regionOptions = regions.map((r) => ({
|
|
2017
|
-
value: r,
|
|
2018
|
-
label: r.charAt(0).toUpperCase() + r.slice(1),
|
|
2019
|
-
}));
|
|
2020
|
-
return regions.length > 1
|
|
2021
|
-
? html`<${SegmentedControl}
|
|
2022
|
-
options=${regionOptions}
|
|
2023
|
-
value=${defaultRegion}
|
|
2024
|
-
onChange=${handleDefaultRegion}
|
|
2025
|
-
/>`
|
|
2026
|
-
: html`<div class="meta-text">Region: ${regions[0]}</div>`;
|
|
2027
|
-
})()}
|
|
2028
|
-
<//>
|
|
2029
|
-
<//>
|
|
2030
|
-
|
|
2031
2110
|
<!-- ─── Advanced ─── -->
|
|
2032
2111
|
<${Collapsible} title=${iconText(":settings: Advanced")} defaultOpen=${false}>
|
|
2033
2112
|
<${Card}>
|
package/ui/tabs/tasks.js
CHANGED
|
@@ -2457,6 +2457,25 @@ export function TaskDetailModal({ task, onClose, onStart, presentation = "modal"
|
|
|
2457
2457
|
task?.assignees,
|
|
2458
2458
|
task?.meta,
|
|
2459
2459
|
]);
|
|
2460
|
+
const lifetimeTotals = task?.lifetimeTotals
|
|
2461
|
+
|| task?.meta?.lifetimeTotals
|
|
2462
|
+
|| task?.runtimeSnapshot?.lifetimeTotals
|
|
2463
|
+
|| null;
|
|
2464
|
+
const lifetimeAttempts = Number(lifetimeTotals?.attemptsCount || 0);
|
|
2465
|
+
const lifetimeTokenCount = Number(lifetimeTotals?.tokenCount || 0);
|
|
2466
|
+
const lifetimeDurationMs = Number(lifetimeTotals?.durationMs || 0);
|
|
2467
|
+
const formatLifetimeDuration = (durationMs) => {
|
|
2468
|
+
const value = Number(durationMs || 0);
|
|
2469
|
+
if (!Number.isFinite(value) || value <= 0) return "0s";
|
|
2470
|
+
const seconds = Math.floor(value / 1000);
|
|
2471
|
+
if (seconds < 60) return `${seconds}s`;
|
|
2472
|
+
const minutes = Math.floor(seconds / 60);
|
|
2473
|
+
const remSeconds = seconds % 60;
|
|
2474
|
+
if (minutes < 60) return remSeconds ? `${minutes}m ${remSeconds}s` : `${minutes}m`;
|
|
2475
|
+
const hours = Math.floor(minutes / 60);
|
|
2476
|
+
const remMinutes = minutes % 60;
|
|
2477
|
+
return remMinutes ? `${hours}h ${remMinutes}m` : `${hours}h`;
|
|
2478
|
+
};
|
|
2460
2479
|
|
|
2461
2480
|
const currentDependencyIds = useMemo(() => normalizeDependencyInput(dependenciesInput), [dependenciesInput]);
|
|
2462
2481
|
const taskCatalogOptions = useMemo(() => (taskCatalog || []).filter((entry) => toText(entry?.id) && toText(entry?.id) !== toText(task?.id)), [taskCatalog, task?.id]);
|
|
@@ -3511,6 +3530,18 @@ export function TaskDetailModal({ task, onClose, onStart, presentation = "modal"
|
|
|
3511
3530
|
style=${{ width: "60px" }}
|
|
3512
3531
|
/>
|
|
3513
3532
|
</div>
|
|
3533
|
+
<div class="task-comment-item">
|
|
3534
|
+
<div class="task-comment-meta">Attempts count</div>
|
|
3535
|
+
<div class="task-comment-body">${lifetimeAttempts.toLocaleString("en-US")}</div>
|
|
3536
|
+
</div>
|
|
3537
|
+
<div class="task-comment-item">
|
|
3538
|
+
<div class="task-comment-meta">Total tokens across all attempts</div>
|
|
3539
|
+
<div class="task-comment-body">${lifetimeTokenCount.toLocaleString("en-US")}</div>
|
|
3540
|
+
</div>
|
|
3541
|
+
<div class="task-comment-item">
|
|
3542
|
+
<div class="task-comment-meta">Total runtime across all attempts</div>
|
|
3543
|
+
<div class="task-comment-body">${formatLifetimeDuration(lifetimeDurationMs)}</div>
|
|
3544
|
+
</div>
|
|
3514
3545
|
</div>
|
|
3515
3546
|
</div>
|
|
3516
3547
|
|
|
@@ -7018,6 +7049,5 @@ function CreateTaskModalInline({ onClose, initialValues = null, sprintOptions =
|
|
|
7018
7049
|
|
|
7019
7050
|
|
|
7020
7051
|
|
|
7021
|
-
|
|
7022
7052
|
|
|
7023
7053
|
|
package/ui/tabs/telemetry.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "@mui/material";
|
|
19
19
|
|
|
20
20
|
import {
|
|
21
|
+
telemetrySummary,
|
|
21
22
|
telemetryErrors,
|
|
22
23
|
telemetryAlerts,
|
|
23
24
|
usageAnalytics,
|
|
@@ -28,6 +29,8 @@ import {
|
|
|
28
29
|
loadTelemetryAlerts,
|
|
29
30
|
loadUsageAnalytics,
|
|
30
31
|
loadShreddingTelemetry,
|
|
32
|
+
loadRetryQueue,
|
|
33
|
+
retryQueueData,
|
|
31
34
|
scheduleRefresh,
|
|
32
35
|
} from "../modules/state.js";
|
|
33
36
|
import {
|
|
@@ -80,6 +83,19 @@ function formatSinceDate(isoStr) {
|
|
|
80
83
|
});
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
function formatDurationMs(ms) {
|
|
87
|
+
const value = Number(ms || 0);
|
|
88
|
+
if (!Number.isFinite(value) || value <= 0) return "0s";
|
|
89
|
+
const seconds = Math.floor(value / 1000);
|
|
90
|
+
if (seconds < 60) return `${seconds}s`;
|
|
91
|
+
const minutes = Math.floor(seconds / 60);
|
|
92
|
+
const remSeconds = seconds % 60;
|
|
93
|
+
if (minutes < 60) return remSeconds ? `${minutes}m ${remSeconds}s` : `${minutes}m`;
|
|
94
|
+
const hours = Math.floor(minutes / 60);
|
|
95
|
+
const remMinutes = minutes % 60;
|
|
96
|
+
return remMinutes ? `${hours}h ${remMinutes}m` : `${hours}h`;
|
|
97
|
+
}
|
|
98
|
+
|
|
83
99
|
function severityChipColor(sev = "medium") {
|
|
84
100
|
const n = String(sev).toLowerCase();
|
|
85
101
|
if (n === "high" || n === "critical") return "error";
|
|
@@ -495,12 +511,17 @@ function ShreddingPanel({ period }) {
|
|
|
495
511
|
|
|
496
512
|
export function TelemetryTab() {
|
|
497
513
|
const data = usageAnalytics.value;
|
|
514
|
+
const retryQueue = retryQueueData.value || { count: 0, items: [], stats: {} };
|
|
515
|
+
const summary = telemetrySummary.value || null;
|
|
516
|
+
const lifetimeTotals = summary?.lifetimeTotals || null;
|
|
498
517
|
const [period, setPeriod] = useState(30);
|
|
499
518
|
const [trendTab, setTrendTab] = useState("agents");
|
|
500
519
|
|
|
501
520
|
useEffect(() => {
|
|
502
521
|
loadUsageAnalytics(period).catch(() => {});
|
|
503
522
|
loadShreddingTelemetry(period).catch(() => {});
|
|
523
|
+
loadRetryQueue().catch(() => {});
|
|
524
|
+
loadTelemetrySummary().catch(() => {});
|
|
504
525
|
}, [period]);
|
|
505
526
|
|
|
506
527
|
const trend = data?.trend;
|
|
@@ -555,6 +576,7 @@ export function TelemetryTab() {
|
|
|
555
576
|
loadTelemetryErrors();
|
|
556
577
|
loadTelemetryExecutors();
|
|
557
578
|
loadTelemetryAlerts();
|
|
579
|
+
loadRetryQueue();
|
|
558
580
|
scheduleRefresh(4000);
|
|
559
581
|
}}>Refresh<//>
|
|
560
582
|
<//>
|
|
@@ -572,6 +594,18 @@ export function TelemetryTab() {
|
|
|
572
594
|
value=${data ? formatCount(data.avgPerDay) : "–"} />
|
|
573
595
|
<${AnalyticsStat} icon="🕐" label="Last Active"
|
|
574
596
|
value=${data?.lastActiveAt ? formatRelative(data.lastActiveAt) : "–"} />
|
|
597
|
+
<${AnalyticsStat} icon="↻" label="Retries Today"
|
|
598
|
+
value=${formatCount(retryQueue?.stats?.totalRetriesToday || 0)} />
|
|
599
|
+
<${AnalyticsStat} icon="⇡" label="Peak Retry Depth"
|
|
600
|
+
value=${formatCount(retryQueue?.stats?.peakRetryDepth || 0)} />
|
|
601
|
+
<${AnalyticsStat} icon="⚠" label="Exhausted Tasks"
|
|
602
|
+
value=${formatCount((retryQueue?.stats?.exhaustedTaskIds || []).length)} />
|
|
603
|
+
<${AnalyticsStat} icon="◈" label="Attempts count"
|
|
604
|
+
value=${formatCount(lifetimeTotals?.attemptsCount || 0)} />
|
|
605
|
+
<${AnalyticsStat} icon="#" label="Total tokens across all attempts"
|
|
606
|
+
value=${formatCount(lifetimeTotals?.tokenCount || 0)} />
|
|
607
|
+
<${AnalyticsStat} icon="⏱" label="Total runtime across all attempts"
|
|
608
|
+
value=${formatDurationMs(lifetimeTotals?.durationMs || 0)} />
|
|
575
609
|
<//>
|
|
576
610
|
|
|
577
611
|
<!-- Activity trend chart -->
|