@chrysb/alphaclaw 0.8.4 → 0.8.6

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 (31) hide show
  1. package/lib/public/css/explorer.css +48 -0
  2. package/lib/public/css/shell.css +149 -0
  3. package/lib/public/css/tailwind.generated.css +1 -1
  4. package/lib/public/css/theme.css +265 -0
  5. package/lib/public/dist/app.bundle.js +2269 -2200
  6. package/lib/public/js/app.js +4 -0
  7. package/lib/public/js/components/icons.js +38 -0
  8. package/lib/public/js/components/models-tab/provider-auth-card.js +60 -49
  9. package/lib/public/js/components/models-tab/use-models.js +74 -9
  10. package/lib/public/js/components/models.js +52 -37
  11. package/lib/public/js/components/onboarding/use-welcome-codex.js +34 -24
  12. package/lib/public/js/components/onboarding/welcome-config.js +76 -10
  13. package/lib/public/js/components/onboarding/welcome-form-step.js +2 -7
  14. package/lib/public/js/components/onboarding/welcome-header.js +12 -14
  15. package/lib/public/js/components/onboarding/welcome-setup-step.js +3 -3
  16. package/lib/public/js/components/providers.js +53 -42
  17. package/lib/public/js/components/sidebar.js +9 -1
  18. package/lib/public/js/components/theme-toggle.js +113 -0
  19. package/lib/public/js/components/welcome/index.js +0 -2
  20. package/lib/public/js/components/welcome/use-welcome.js +101 -36
  21. package/lib/public/js/lib/codex-oauth-window.js +22 -0
  22. package/lib/public/js/lib/model-catalog.js +20 -0
  23. package/lib/public/js/lib/storage-keys.js +1 -1
  24. package/lib/public/login.html +8 -4
  25. package/lib/public/setup.html +9 -0
  26. package/lib/server/alphaclaw-version.js +60 -13
  27. package/lib/server/db/webhooks/index.js +48 -8
  28. package/lib/server/model-catalog-cache.js +251 -0
  29. package/lib/server/routes/models.js +14 -23
  30. package/lib/server/routes/webhooks.js +12 -1
  31. package/package.json +1 -1
@@ -6,6 +6,7 @@ import {
6
6
  applyImport,
7
7
  fetchModels,
8
8
  } from "../../lib/api.js";
9
+ import { useCachedFetch } from "../../hooks/use-cached-fetch.js";
9
10
  import {
10
11
  getModelProvider,
11
12
  getAuthProviderFromModelProvider,
@@ -13,8 +14,15 @@ import {
13
14
  getVisibleAiFieldKeys,
14
15
  kProviderAuthFields,
15
16
  } from "../../lib/model-config.js";
17
+ import {
18
+ getInitialOnboardingModelKey,
19
+ getModelCatalogModels,
20
+ kModelCatalogCacheKey,
21
+ } from "../../lib/model-catalog.js";
16
22
  import {
17
23
  kWelcomeGroups,
24
+ getWelcomeGroupError,
25
+ findFirstInvalidWelcomeGroup,
18
26
  isValidGithubRepoInput,
19
27
  kGithubFlowFresh,
20
28
  kGithubFlowImport,
@@ -76,11 +84,18 @@ const normalizePlaceholderReview = (review) => {
76
84
  export const useWelcome = ({ onComplete }) => {
77
85
  const kSetupStepIndex = kWelcomeGroups.length;
78
86
  const kPairingStepIndex = kSetupStepIndex + 1;
79
- const { vals, setVals, setValue, step, setStep, setupError, setSetupError } =
80
- useWelcomeStorage({
81
- kSetupStepIndex,
82
- kPairingStepIndex,
83
- });
87
+ const {
88
+ vals,
89
+ setVals,
90
+ setValue: setStoredValue,
91
+ step,
92
+ setStep,
93
+ setupError,
94
+ setSetupError,
95
+ } = useWelcomeStorage({
96
+ kSetupStepIndex,
97
+ kPairingStepIndex,
98
+ });
84
99
  const [models, setModels] = useState([]);
85
100
  const [modelsLoading, setModelsLoading] = useState(true);
86
101
  const [modelsError, setModelsError] = useState(null);
@@ -110,6 +125,14 @@ export const useWelcome = ({ onComplete }) => {
110
125
  const [importScanResult, setImportScanResult] = useState(null);
111
126
  const [importScanning, setImportScanning] = useState(false);
112
127
  const [importError, setImportError] = useState(null);
128
+ const modelsFetchState = useCachedFetch(kModelCatalogCacheKey, fetchModels, {
129
+ maxAgeMs: 30000,
130
+ });
131
+
132
+ const setValue = (key, value) => {
133
+ if (formError) setFormError(null);
134
+ setStoredValue(key, value);
135
+ };
113
136
 
114
137
  const setImportStep = (nextStep) => {
115
138
  setImportStepState(nextStep);
@@ -129,21 +152,54 @@ export const useWelcome = ({ onComplete }) => {
129
152
  };
130
153
 
131
154
  useEffect(() => {
132
- fetchModels()
133
- .then((result) => {
134
- const list = Array.isArray(result.models) ? result.models : [];
135
- const featured = getFeaturedModels(list);
136
- setModels(list);
137
- if (!vals.MODEL_KEY && list.length > 0) {
138
- const defaultModel = featured[0] || list[0];
139
- setVals((prev) => ({ ...prev, MODEL_KEY: defaultModel.key }));
140
- }
141
- })
142
- .catch(() => setModelsError("Failed to load models"))
143
- .finally(() => setModelsLoading(false));
144
- }, []);
155
+ const list = getModelCatalogModels(modelsFetchState.data);
156
+ if (!modelsFetchState.data) return;
157
+ setModels(list);
158
+ setModelsError(list.length > 0 ? null : "No models found");
159
+ const defaultModelKey = getInitialOnboardingModelKey({
160
+ catalog: list,
161
+ currentModelKey: vals.MODEL_KEY,
162
+ });
163
+ if (!vals.MODEL_KEY && defaultModelKey) {
164
+ setVals((prev) => ({ ...prev, MODEL_KEY: defaultModelKey }));
165
+ }
166
+ }, [modelsFetchState.data, setVals, vals.MODEL_KEY]);
167
+
168
+ useEffect(() => {
169
+ const hasModels = getModelCatalogModels(modelsFetchState.data).length > 0;
170
+ setModelsLoading(modelsFetchState.loading && !hasModels);
171
+ }, [modelsFetchState.data, modelsFetchState.loading]);
172
+
173
+ useEffect(() => {
174
+ if (!modelsFetchState.error) return;
175
+ setModelsError("Failed to load models");
176
+ setModelsLoading(false);
177
+ }, [modelsFetchState.error]);
178
+
179
+ const getValidationContext = (currentVals = {}) => {
180
+ const currentSelectedProvider = getModelProvider(
181
+ String(currentVals.MODEL_KEY || "").trim(),
182
+ );
183
+ const currentSelectedAuthProvider =
184
+ getAuthProviderFromModelProvider(currentSelectedProvider);
185
+ const currentProviderAuthFields =
186
+ kProviderAuthFields[currentSelectedAuthProvider] || [];
187
+ const currentHasAi =
188
+ currentSelectedProvider === "openai-codex"
189
+ ? !!codexStatus.connected
190
+ : currentProviderAuthFields.some((field) =>
191
+ !!String(currentVals[field.key] || "").trim(),
192
+ );
145
193
 
146
- const selectedProvider = getModelProvider(vals.MODEL_KEY);
194
+ return {
195
+ hasAi: currentHasAi,
196
+ selectedProvider: currentSelectedProvider,
197
+ codexLoading,
198
+ };
199
+ };
200
+
201
+ const validationContext = getValidationContext(vals);
202
+ const { selectedProvider, hasAi } = validationContext;
147
203
  const placeholderReview = normalizePlaceholderReview(
148
204
  vals[kImportPlaceholderReviewKey],
149
205
  );
@@ -164,23 +220,10 @@ export const useWelcome = ({ onComplete }) => {
164
220
  const canToggleFullCatalog =
165
221
  featuredModels.length > 0 && models.length > featuredModels.length;
166
222
  const visibleAiFieldKeys = getVisibleAiFieldKeys(selectedProvider);
167
- const selectedAuthProvider = getAuthProviderFromModelProvider(selectedProvider);
168
- const selectedProviderAuthFields = kProviderAuthFields[selectedAuthProvider] || [];
169
- const hasAi =
170
- selectedProvider === "openai-codex"
171
- ? !!codexStatus.connected
172
- : selectedProviderAuthFields.some(
173
- (field) => !!String(vals[field.key] || "").trim(),
174
- );
175
-
176
- const allValid = kWelcomeGroups.every((group) => group.validate(vals, { hasAi }));
177
223
  const isPreStep = step === -1;
178
224
  const isSetupStep = step === kSetupStepIndex;
179
225
  const isPairingStep = step === kPairingStepIndex;
180
226
  const activeGroup = step >= 0 && step < kSetupStepIndex ? kWelcomeGroups[step] : null;
181
- const currentGroupValid = activeGroup
182
- ? activeGroup.validate(vals, { hasAi })
183
- : false;
184
227
  const selectedPairingChannel = String(
185
228
  vals[kPairingChannelKey] || getPreferredPairingChannel(vals),
186
229
  );
@@ -202,7 +245,21 @@ export const useWelcome = ({ onComplete }) => {
202
245
  const handleSubmit = async () => {
203
246
  const { normalizedVals, didChange } = normalizeOnboardingVals(vals);
204
247
  if (didChange) setVals(normalizedVals);
205
- if (!kWelcomeGroups.every((group) => group.validate(normalizedVals, { hasAi }))) {
248
+ const submitValidationContext = getValidationContext(normalizedVals);
249
+ const invalidGroup = findFirstInvalidWelcomeGroup(
250
+ normalizedVals,
251
+ submitValidationContext,
252
+ );
253
+ if (invalidGroup) {
254
+ setFormError(
255
+ getWelcomeGroupError(
256
+ invalidGroup.id,
257
+ normalizedVals,
258
+ submitValidationContext,
259
+ ),
260
+ );
261
+ setSetupError(null);
262
+ setStep(kWelcomeGroups.findIndex((group) => group.id === invalidGroup.id));
206
263
  return;
207
264
  }
208
265
  if (loading) return;
@@ -309,7 +366,17 @@ export const useWelcome = ({ onComplete }) => {
309
366
  const goNext = async () => {
310
367
  const { normalizedVals, didChange } = normalizeOnboardingVals(vals);
311
368
  if (didChange) setVals(normalizedVals);
312
- if (!activeGroup || !activeGroup.validate(normalizedVals, { hasAi })) return;
369
+ if (!activeGroup) return;
370
+ const stepValidationContext = getValidationContext(normalizedVals);
371
+ const stepValidationError = getWelcomeGroupError(
372
+ activeGroup.id,
373
+ normalizedVals,
374
+ stepValidationContext,
375
+ );
376
+ if (stepValidationError) {
377
+ setFormError(stepValidationError);
378
+ return;
379
+ }
313
380
  setFormError(null);
314
381
  if (activeGroup.id === "github") {
315
382
  const githubFlow = normalizedVals._GITHUB_FLOW || kGithubFlowFresh;
@@ -545,12 +612,10 @@ export const useWelcome = ({ onComplete }) => {
545
612
  canToggleFullCatalog,
546
613
  visibleAiFieldKeys,
547
614
  hasAi,
548
- allValid,
549
615
  isPreStep,
550
616
  isSetupStep,
551
617
  isPairingStep,
552
618
  activeGroup,
553
- currentGroupValid,
554
619
  selectedPairingChannel,
555
620
  placeholderReview,
556
621
  isImportStep,
@@ -0,0 +1,22 @@
1
+ const kCodexAuthStartPath = "/auth/codex/start";
2
+ const kCodexAuthWindowName = "codex-auth";
3
+ const kCodexAuthPopupFeatures = "popup=yes,width=640,height=780";
4
+ const kCodexAuthCallbackMessageType = "callback-input";
5
+
6
+ export const openCodexAuthWindow = () => {
7
+ const popup = window.open(
8
+ kCodexAuthStartPath,
9
+ kCodexAuthWindowName,
10
+ kCodexAuthPopupFeatures,
11
+ );
12
+ if (!popup || popup.closed) {
13
+ window.location.href = kCodexAuthStartPath;
14
+ return null;
15
+ }
16
+ return popup;
17
+ };
18
+
19
+ export const isCodexAuthCallbackMessage = (value) =>
20
+ value?.codex === kCodexAuthCallbackMessageType &&
21
+ typeof value.input === "string" &&
22
+ value.input.trim().length > 0;
@@ -0,0 +1,20 @@
1
+ import { getFeaturedModels } from "./model-config.js";
2
+
3
+ export const kModelCatalogCacheKey = "/api/models";
4
+ export const kModelCatalogPollIntervalMs = 3000;
5
+
6
+ export const getModelCatalogModels = (payload) =>
7
+ Array.isArray(payload?.models) ? payload.models : [];
8
+
9
+ export const isModelCatalogRefreshing = (payload) =>
10
+ Boolean(payload?.refreshing);
11
+
12
+ export const getInitialOnboardingModelKey = ({
13
+ catalog = [],
14
+ currentModelKey = "",
15
+ } = {}) => {
16
+ const normalizedCurrent = String(currentModelKey || "").trim();
17
+ if (normalizedCurrent) return normalizedCurrent;
18
+ const featuredModels = getFeaturedModels(catalog);
19
+ return String(featuredModels[0]?.key || catalog[0]?.key || "");
20
+ };
@@ -6,6 +6,7 @@
6
6
 
7
7
  // --- UI settings (single JSON blob containing sub-keys) ---
8
8
  export const kUiSettingsStorageKey = "alphaclaw.ui.settings";
9
+ export const kThemeStorageKey = "alphaclaw.ui.theme";
9
10
 
10
11
  // --- Browse / file viewer ---
11
12
  export const kFileViewerModeStorageKey = "alphaclaw.browse.viewerMode";
@@ -30,4 +31,3 @@ export const kAgentLastSessionKey = "alphaclaw.agent.lastSessionKey";
30
31
 
31
32
  // --- Chat ---
32
33
  export const kChatSessionDraftsStorageKey = "alphaclaw.chat.sessionDrafts";
33
-
@@ -11,6 +11,14 @@
11
11
  <link rel="icon" type="image/svg+xml" href="./img/logo.svg" />
12
12
  <link rel="stylesheet" href="./css/theme.css" />
13
13
  <link rel="stylesheet" href="./css/tailwind.generated.css" />
14
+ <script>
15
+ try {
16
+ var t = localStorage.getItem("alphaclaw.ui.theme");
17
+ if (t === "system") t = window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
18
+ else if (t !== "dark" && t !== "light") t = "dark";
19
+ document.documentElement.dataset.theme = t;
20
+ } catch {}
21
+ </script>
14
22
  </head>
15
23
  <body class="min-h-screen flex items-center justify-center p-4">
16
24
  <div class="max-w-sm w-full relative z-10">
@@ -53,10 +61,6 @@
53
61
  </form>
54
62
  </div>
55
63
  <script>
56
- try {
57
- window.localStorage?.clear?.();
58
- } catch {}
59
-
60
64
  const formEl = document.getElementById("login-form");
61
65
  const passwordEl = document.getElementById("password");
62
66
  const submitButtonEl = document.getElementById("submit-btn");
@@ -16,6 +16,15 @@
16
16
  <link rel="stylesheet" href="./css/agents.css" />
17
17
  <link rel="stylesheet" href="./css/chat.css" />
18
18
  <link rel="stylesheet" href="./css/cron.css" />
19
+ <script>
20
+ // Apply saved theme before render to prevent flash.
21
+ try {
22
+ var t = localStorage.getItem("alphaclaw.ui.theme");
23
+ if (t === "system") t = window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
24
+ else if (t !== "dark" && t !== "light") t = "dark";
25
+ document.documentElement.dataset.theme = t;
26
+ } catch {}
27
+ </script>
19
28
  </head>
20
29
  <body>
21
30
  <div id="app"></div>
@@ -1,5 +1,6 @@
1
1
  const childProcess = require("child_process");
2
2
  const fs = require("fs");
3
+ const os = require("os");
3
4
  const path = require("path");
4
5
  const https = require("https");
5
6
  const http = require("http");
@@ -7,6 +8,7 @@ const {
7
8
  kLatestVersionCacheTtlMs,
8
9
  kAlphaclawRegistryUrl,
9
10
  kNpmPackageRoot,
11
+ kOpenclawUpdateCopyTimeoutMs,
10
12
  kRootDir,
11
13
  } = require("./constants");
12
14
 
@@ -114,7 +116,7 @@ const createAlphaclawVersionService = () => {
114
116
  const parent = path.dirname(dir);
115
117
  if (
116
118
  path.basename(parent) === "node_modules" ||
117
- parent.includes("node_modules")
119
+ parent.includes(`${path.sep}node_modules${path.sep}`)
118
120
  ) {
119
121
  dir = parent;
120
122
  continue;
@@ -123,7 +125,11 @@ const createAlphaclawVersionService = () => {
123
125
  if (fs.existsSync(pkgPath)) {
124
126
  try {
125
127
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
126
- if (pkg.dependencies?.["@chrysb/alphaclaw"]) {
128
+ if (
129
+ pkg.dependencies?.["@chrysb/alphaclaw"] ||
130
+ pkg.devDependencies?.["@chrysb/alphaclaw"] ||
131
+ pkg.optionalDependencies?.["@chrysb/alphaclaw"]
132
+ ) {
127
133
  return parent;
128
134
  }
129
135
  } catch {}
@@ -137,19 +143,39 @@ const createAlphaclawVersionService = () => {
137
143
  const installLatestAlphaclaw = () =>
138
144
  new Promise((resolve, reject) => {
139
145
  const installDir = findInstallDir();
146
+ const tmpDir = fs.mkdtempSync(
147
+ path.join(os.tmpdir(), "alphaclaw-update-"),
148
+ );
149
+
150
+ const cleanup = () => {
151
+ try {
152
+ fs.rmSync(tmpDir, { recursive: true, force: true });
153
+ } catch {}
154
+ };
155
+
156
+ fs.writeFileSync(
157
+ path.join(tmpDir, "package.json"),
158
+ JSON.stringify({
159
+ private: true,
160
+ dependencies: { "@chrysb/alphaclaw": "latest" },
161
+ }),
162
+ );
163
+
164
+ const npmEnv = {
165
+ ...process.env,
166
+ npm_config_update_notifier: "false",
167
+ npm_config_fund: "false",
168
+ npm_config_audit: "false",
169
+ };
170
+
140
171
  console.log(
141
- `[alphaclaw] Running: npm install @chrysb/alphaclaw@latest (cwd: ${installDir})`,
172
+ `[alphaclaw] Running: npm install @chrysb/alphaclaw@latest in temp dir (target: ${installDir})`,
142
173
  );
143
174
  childProcess.exec(
144
- "npm install @chrysb/alphaclaw@latest --omit=dev --no-save --save=false --package-lock=false --prefer-online",
175
+ "npm install --omit=dev --prefer-online --package-lock=false",
145
176
  {
146
- cwd: installDir,
147
- env: {
148
- ...process.env,
149
- npm_config_update_notifier: "false",
150
- npm_config_fund: "false",
151
- npm_config_audit: "false",
152
- },
177
+ cwd: tmpDir,
178
+ env: npmEnv,
153
179
  timeout: 180000,
154
180
  },
155
181
  (err, stdout, stderr) => {
@@ -158,6 +184,7 @@ const createAlphaclawVersionService = () => {
158
184
  console.log(
159
185
  `[alphaclaw] alphaclaw install error: ${message.slice(0, 200)}`,
160
186
  );
187
+ cleanup();
161
188
  return reject(
162
189
  new Error(
163
190
  message || "Failed to install @chrysb/alphaclaw@latest",
@@ -169,8 +196,28 @@ const createAlphaclawVersionService = () => {
169
196
  `[alphaclaw] alphaclaw install stdout: ${stdout.trim().slice(0, 300)}`,
170
197
  );
171
198
  }
172
- console.log("[alphaclaw] alphaclaw install completed");
173
- resolve({ stdout: stdout?.trim(), stderr: stderr?.trim() });
199
+
200
+ const src = path.join(tmpDir, "node_modules");
201
+ const dest = path.join(installDir, "node_modules");
202
+ childProcess.exec(
203
+ `cp -af "${src}/." "${dest}/"`,
204
+ { timeout: kOpenclawUpdateCopyTimeoutMs },
205
+ (copyErr) => {
206
+ cleanup();
207
+ if (copyErr) {
208
+ console.log(
209
+ `[alphaclaw] alphaclaw copy error: ${(copyErr.message || "").slice(0, 200)}`,
210
+ );
211
+ return reject(
212
+ new Error(
213
+ `Failed to copy updated AlphaClaw files: ${copyErr.message}`,
214
+ ),
215
+ );
216
+ }
217
+ console.log("[alphaclaw] alphaclaw install completed");
218
+ resolve({ stdout: stdout?.trim(), stderr: stderr?.trim() });
219
+ },
220
+ );
174
221
  },
175
222
  );
176
223
  });
@@ -10,6 +10,7 @@ let pruneTimer = null;
10
10
  const kDefaultRequestLimit = 50;
11
11
  const kMaxRequestLimit = 200;
12
12
  const kPruneIntervalMs = 12 * 60 * 60 * 1000;
13
+ const kHealthSummaryWindow = 25;
13
14
 
14
15
  const ensureDb = () => {
15
16
  if (!db) throw new Error("Webhooks DB not initialized");
@@ -202,22 +203,61 @@ const getHookSummaries = () => {
202
203
  const database = ensureDb();
203
204
  const rows = database
204
205
  .prepare(`
206
+ WITH ranked_requests AS (
207
+ SELECT
208
+ hook_name,
209
+ created_at,
210
+ gateway_status,
211
+ ROW_NUMBER() OVER (
212
+ PARTITION BY hook_name
213
+ ORDER BY created_at DESC, id DESC
214
+ ) AS row_num
215
+ FROM webhook_requests
216
+ ),
217
+ overall_counts AS (
218
+ SELECT
219
+ hook_name,
220
+ MAX(created_at) AS last_received,
221
+ COUNT(*) AS total_count,
222
+ SUM(CASE WHEN gateway_status >= 200 AND gateway_status < 300 THEN 1 ELSE 0 END) AS success_count,
223
+ SUM(CASE WHEN gateway_status IS NULL OR gateway_status < 200 OR gateway_status >= 300 THEN 1 ELSE 0 END) AS error_count
224
+ FROM webhook_requests
225
+ GROUP BY hook_name
226
+ ),
227
+ recent_counts AS (
228
+ SELECT
229
+ hook_name,
230
+ COUNT(*) AS recent_total_count,
231
+ SUM(CASE WHEN gateway_status >= 200 AND gateway_status < 300 THEN 1 ELSE 0 END) AS recent_success_count,
232
+ SUM(CASE WHEN gateway_status IS NULL OR gateway_status < 200 OR gateway_status >= 300 THEN 1 ELSE 0 END) AS recent_error_count
233
+ FROM ranked_requests
234
+ WHERE row_num <= $health_window
235
+ GROUP BY hook_name
236
+ )
205
237
  SELECT
206
- hook_name,
207
- MAX(created_at) AS last_received,
208
- COUNT(*) AS total_count,
209
- SUM(CASE WHEN gateway_status >= 200 AND gateway_status < 300 THEN 1 ELSE 0 END) AS success_count,
210
- SUM(CASE WHEN gateway_status IS NULL OR gateway_status < 200 OR gateway_status >= 300 THEN 1 ELSE 0 END) AS error_count
211
- FROM webhook_requests
212
- GROUP BY hook_name
238
+ overall_counts.hook_name,
239
+ overall_counts.last_received,
240
+ overall_counts.total_count,
241
+ overall_counts.success_count,
242
+ overall_counts.error_count,
243
+ COALESCE(recent_counts.recent_total_count, 0) AS recent_total_count,
244
+ COALESCE(recent_counts.recent_success_count, 0) AS recent_success_count,
245
+ COALESCE(recent_counts.recent_error_count, 0) AS recent_error_count
246
+ FROM overall_counts
247
+ LEFT JOIN recent_counts
248
+ ON recent_counts.hook_name = overall_counts.hook_name
213
249
  `)
214
- .all();
250
+ .all({ $health_window: kHealthSummaryWindow });
215
251
  return rows.map((row) => ({
216
252
  hookName: row.hook_name,
217
253
  lastReceived: row.last_received || null,
218
254
  totalCount: Number(row.total_count || 0),
219
255
  successCount: Number(row.success_count || 0),
220
256
  errorCount: Number(row.error_count || 0),
257
+ recentTotalCount: Number(row.recent_total_count || 0),
258
+ recentSuccessCount: Number(row.recent_success_count || 0),
259
+ recentErrorCount: Number(row.recent_error_count || 0),
260
+ healthWindowSize: kHealthSummaryWindow,
221
261
  }));
222
262
  };
223
263