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.
Files changed (64) hide show
  1. package/.env.example +8 -0
  2. package/README.md +20 -0
  3. package/agent/agent-event-bus.mjs +248 -6
  4. package/agent/agent-pool.mjs +125 -28
  5. package/agent/agent-work-analyzer.mjs +8 -16
  6. package/agent/retry-queue.mjs +164 -0
  7. package/bosun.config.example.json +25 -0
  8. package/bosun.schema.json +825 -183
  9. package/cli.mjs +59 -5
  10. package/config/config.mjs +130 -3
  11. package/infra/monitor.mjs +693 -67
  12. package/infra/runtime-accumulator.mjs +376 -84
  13. package/infra/session-tracker.mjs +82 -25
  14. package/lib/codebase-audit.mjs +133 -18
  15. package/package.json +23 -4
  16. package/server/setup-web-server.mjs +25 -0
  17. package/server/ui-server.mjs +248 -29
  18. package/setup.mjs +27 -24
  19. package/shell/codex-shell.mjs +34 -3
  20. package/shell/copilot-shell.mjs +50 -8
  21. package/task/msg-hub.mjs +193 -0
  22. package/task/pipeline.mjs +544 -0
  23. package/task/task-cli.mjs +38 -2
  24. package/task/task-executor-pipeline.mjs +143 -0
  25. package/task/task-executor.mjs +36 -27
  26. package/telegram/get-telegram-chat-id.mjs +57 -47
  27. package/ui/components/workspace-switcher.js +7 -7
  28. package/ui/demo-defaults.js +15694 -10573
  29. package/ui/modules/settings-schema.js +2 -0
  30. package/ui/modules/state.js +54 -57
  31. package/ui/modules/voice-client-sdk.js +375 -36
  32. package/ui/modules/voice-client.js +140 -31
  33. package/ui/setup.html +68 -2
  34. package/ui/styles/components.css +57 -0
  35. package/ui/styles.css +201 -1
  36. package/ui/tabs/dashboard.js +74 -0
  37. package/ui/tabs/logs.js +10 -0
  38. package/ui/tabs/settings.js +178 -99
  39. package/ui/tabs/tasks.js +31 -1
  40. package/ui/tabs/telemetry.js +34 -0
  41. package/ui/tabs/workflow-canvas-utils.mjs +8 -1
  42. package/ui/tabs/workflows.js +532 -275
  43. package/voice/voice-agents-sdk.mjs +1 -1
  44. package/voice/voice-relay.mjs +6 -6
  45. package/workflow/declarative-workflows.mjs +145 -0
  46. package/workflow/msg-hub.mjs +237 -0
  47. package/workflow/pipeline-workflows.mjs +287 -0
  48. package/workflow/pipeline.mjs +828 -315
  49. package/workflow/workflow-cli.mjs +128 -0
  50. package/workflow/workflow-engine.mjs +329 -17
  51. package/workflow/workflow-nodes/custom-loader.mjs +250 -0
  52. package/workflow/workflow-nodes.mjs +1955 -223
  53. package/workflow/workflow-templates.mjs +26 -8
  54. package/workflow-templates/agents.mjs +0 -1
  55. package/workflow-templates/bosun-native.mjs +212 -2
  56. package/workflow-templates/continuation-loop.mjs +339 -0
  57. package/workflow-templates/github.mjs +516 -40
  58. package/workflow-templates/planning.mjs +446 -17
  59. package/workflow-templates/reliability.mjs +65 -12
  60. package/workflow-templates/task-batch.mjs +24 -8
  61. package/workflow-templates/task-lifecycle.mjs +83 -6
  62. package/workspace/context-cache.mjs +66 -18
  63. package/workspace/workspace-manager.mjs +2 -1
  64. package/workflow-templates/issue-continuation.mjs +0 -243
@@ -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) {
@@ -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
- showToast("Settings take effect after auto-reload (~2 seconds)", "info");
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
- : html`
1473
- <${Card}>
1474
- ${catDefs.map((def) => renderSetting(def))}
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>${changeCount > 0 ? `${changeCount} unsaved change${changeCount !== 1 ? "s" : ""}` : "All changes saved"}</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. The server will auto-reload (~2 seconds).
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, dmp, ds, dr] = await Promise.all([
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
 
@@ -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 -->