@chrysb/alphaclaw 0.9.27 → 0.9.29
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/README.md +11 -4
- package/bin/alphaclaw.js +59 -0
- package/lib/public/dist/app.bundle.js +1143 -1129
- package/lib/public/js/components/doctor/findings-list.js +18 -1
- package/lib/public/js/components/doctor/fix-card-modal.js +8 -17
- package/lib/public/js/components/doctor/helpers.js +7 -1
- package/lib/public/js/components/doctor/index.js +1 -1
- package/lib/public/js/components/onboarding/welcome-config.js +6 -2
- package/lib/public/js/components/onboarding/welcome-setup-step.js +58 -4
- package/lib/public/js/components/welcome/use-welcome.js +15 -8
- package/lib/public/js/lib/api.js +7 -2
- package/lib/public/js/lib/model-config.js +30 -8
- package/lib/server/db/doctor/index.js +94 -5
- package/lib/server/db/doctor/schema.js +4 -0
- package/lib/server/doctor/constants.js +1 -0
- package/lib/server/doctor/fix-completion.js +6 -0
- package/lib/server/doctor/service.js +93 -14
- package/lib/server/onboarding/index.js +11 -0
- package/lib/server/onboarding/validation.js +14 -3
- package/lib/server/routes/doctor.js +5 -2
- package/lib/server/routes/onboarding.js +33 -0
- package/lib/server.js +5 -0
- package/package.json +1 -1
|
@@ -202,6 +202,9 @@ export const DoctorFindingsList = ({
|
|
|
202
202
|
<${Badge} tone=${getDoctorCategoryTone(card.category)}>
|
|
203
203
|
${formatDoctorCategory(card.category)}
|
|
204
204
|
</${Badge}>
|
|
205
|
+
${card.status === "working"
|
|
206
|
+
? html`<${Badge} tone="info">Working</${Badge}>`
|
|
207
|
+
: null}
|
|
205
208
|
${
|
|
206
209
|
showRunMeta
|
|
207
210
|
? html`
|
|
@@ -314,8 +317,13 @@ export const DoctorFindingsList = ({
|
|
|
314
317
|
<${ActionButton}
|
|
315
318
|
onClick=${() => onAskAgentFix(card)}
|
|
316
319
|
loading=${busyCardId === card.id}
|
|
320
|
+
disabled=${card.status !== "open"}
|
|
317
321
|
tone="primary"
|
|
318
|
-
idleLabel
|
|
322
|
+
idleLabel=${card.status === "working"
|
|
323
|
+
? "Agent working..."
|
|
324
|
+
: card.status === "open"
|
|
325
|
+
? "Ask agent to fix"
|
|
326
|
+
: "Reopen to send"}
|
|
319
327
|
loadingLabel="Sending..."
|
|
320
328
|
/>
|
|
321
329
|
${
|
|
@@ -335,6 +343,15 @@ export const DoctorFindingsList = ({
|
|
|
335
343
|
/>
|
|
336
344
|
`
|
|
337
345
|
}
|
|
346
|
+
${card.status === "working"
|
|
347
|
+
? html`
|
|
348
|
+
<${ActionButton}
|
|
349
|
+
onClick=${() => onUpdateStatus(card, "open")}
|
|
350
|
+
tone="secondary"
|
|
351
|
+
idleLabel="Reopen"
|
|
352
|
+
/>
|
|
353
|
+
`
|
|
354
|
+
: null}
|
|
338
355
|
${
|
|
339
356
|
card.status !== "dismissed"
|
|
340
357
|
? html`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { h } from "preact";
|
|
2
2
|
import htm from "htm";
|
|
3
|
-
import { sendDoctorCardFix
|
|
3
|
+
import { sendDoctorCardFix } from "../../lib/api.js";
|
|
4
4
|
import { showToast } from "../toast.js";
|
|
5
5
|
import { AgentSendModal } from "../agent-send-modal.js";
|
|
6
6
|
|
|
@@ -12,29 +12,20 @@ export const DoctorFixCardModal = ({
|
|
|
12
12
|
onClose = () => {},
|
|
13
13
|
onComplete = () => {},
|
|
14
14
|
}) => {
|
|
15
|
-
const handleSend = async ({ selectedSession, message }) => {
|
|
15
|
+
const handleSend = async ({ selectedSession, selectedSessionKey, message }) => {
|
|
16
16
|
if (!card?.id) return false;
|
|
17
17
|
try {
|
|
18
18
|
await sendDoctorCardFix({
|
|
19
19
|
cardId: card.id,
|
|
20
|
-
|
|
20
|
+
sessionKey: selectedSessionKey,
|
|
21
21
|
replyChannel: selectedSession?.replyChannel || "",
|
|
22
22
|
replyTo: selectedSession?.replyTo || "",
|
|
23
23
|
prompt: message,
|
|
24
24
|
});
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"success",
|
|
30
|
-
);
|
|
31
|
-
} catch (statusError) {
|
|
32
|
-
showToast(
|
|
33
|
-
statusError.message ||
|
|
34
|
-
"Doctor fix request sent, but could not mark the finding fixed",
|
|
35
|
-
"warning",
|
|
36
|
-
);
|
|
37
|
-
}
|
|
25
|
+
showToast(
|
|
26
|
+
"Doctor fix queued. The agent will mark it fixed after applying and verifying the change.",
|
|
27
|
+
"success",
|
|
28
|
+
);
|
|
38
29
|
await onComplete();
|
|
39
30
|
return true;
|
|
40
31
|
} catch (error) {
|
|
@@ -51,7 +42,7 @@ export const DoctorFixCardModal = ({
|
|
|
51
42
|
initialMessage=${String(card?.fixPrompt || "")}
|
|
52
43
|
resetKey=${String(card?.id || "")}
|
|
53
44
|
submitLabel="Send fix request"
|
|
54
|
-
loadingLabel="
|
|
45
|
+
loadingLabel="Queuing..."
|
|
55
46
|
onClose=${onClose}
|
|
56
47
|
onSubmit=${handleSend}
|
|
57
48
|
/>
|
|
@@ -12,6 +12,7 @@ export const getDoctorStatusTone = (status = "") => {
|
|
|
12
12
|
.trim()
|
|
13
13
|
.toLowerCase();
|
|
14
14
|
if (normalized === "fixed") return "success";
|
|
15
|
+
if (normalized === "working") return "info";
|
|
15
16
|
if (normalized === "dismissed") return "neutral";
|
|
16
17
|
return "warning";
|
|
17
18
|
};
|
|
@@ -60,6 +61,10 @@ export const groupDoctorCardsByStatus = (cards = []) =>
|
|
|
60
61
|
groups.fixed.push(card);
|
|
61
62
|
return groups;
|
|
62
63
|
}
|
|
64
|
+
if (status === "working") {
|
|
65
|
+
groups.working.push(card);
|
|
66
|
+
return groups;
|
|
67
|
+
}
|
|
63
68
|
if (status === "dismissed") {
|
|
64
69
|
groups.dismissed.push(card);
|
|
65
70
|
return groups;
|
|
@@ -67,7 +72,7 @@ export const groupDoctorCardsByStatus = (cards = []) =>
|
|
|
67
72
|
groups.open.push(card);
|
|
68
73
|
return groups;
|
|
69
74
|
},
|
|
70
|
-
{ open: [], dismissed: [], fixed: [] },
|
|
75
|
+
{ open: [], working: [], dismissed: [], fixed: [] },
|
|
71
76
|
);
|
|
72
77
|
|
|
73
78
|
export const shouldShowDoctorWarning = (
|
|
@@ -185,6 +190,7 @@ export const buildDoctorRunMarkers = (run = null) => {
|
|
|
185
190
|
|
|
186
191
|
export const buildDoctorStatusFilterOptions = () => [
|
|
187
192
|
{ value: "open", label: "Open" },
|
|
193
|
+
{ value: "working", label: "Working" },
|
|
188
194
|
{ value: "dismissed", label: "Dismissed" },
|
|
189
195
|
{ value: "fixed", label: "Fixed" },
|
|
190
196
|
];
|
|
@@ -78,7 +78,7 @@ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
|
|
|
78
78
|
status: "running",
|
|
79
79
|
summary: "",
|
|
80
80
|
priorityCounts: { P0: 0, P1: 0, P2: 0 },
|
|
81
|
-
statusCounts: { open: 0, dismissed: 0, fixed: 0 },
|
|
81
|
+
statusCounts: { open: 0, working: 0, dismissed: 0, fixed: 0 },
|
|
82
82
|
}
|
|
83
83
|
: null;
|
|
84
84
|
const displayRuns = pendingRun ? [pendingRun, ...runs] : runs;
|
|
@@ -53,12 +53,16 @@ const getAiGroupError = (vals, ctx = {}) => {
|
|
|
53
53
|
if (!hasValue(vals.MODEL_KEY) || !String(vals.MODEL_KEY).includes("/")) {
|
|
54
54
|
return "Choose a model to continue.";
|
|
55
55
|
}
|
|
56
|
-
if (
|
|
56
|
+
if (
|
|
57
|
+
ctx.selectedProvider === "openai-codex" &&
|
|
58
|
+
ctx.codexLoading &&
|
|
59
|
+
!ctx.hasAi
|
|
60
|
+
) {
|
|
57
61
|
return "Checking Codex OAuth status. Try Next again in a moment.";
|
|
58
62
|
}
|
|
59
63
|
if (!ctx.hasAi) {
|
|
60
64
|
return ctx.selectedProvider === "openai-codex"
|
|
61
|
-
? "Connect Codex OAuth to continue."
|
|
65
|
+
? "Connect Codex OAuth or enter an OpenAI API key to continue."
|
|
62
66
|
: "Add credentials for the selected model provider to continue.";
|
|
63
67
|
}
|
|
64
68
|
return "";
|
|
@@ -2,6 +2,7 @@ import { h } from "preact";
|
|
|
2
2
|
import { useEffect, useState } from "preact/hooks";
|
|
3
3
|
import htm from "htm";
|
|
4
4
|
import { LoadingSpinner } from "../loading-spinner.js";
|
|
5
|
+
import { fetchOnboardProgress } from "../../lib/api.js";
|
|
5
6
|
|
|
6
7
|
const html = htm.bind(h);
|
|
7
8
|
const kSetupTips = [
|
|
@@ -30,9 +31,24 @@ const kSetupTips = [
|
|
|
30
31
|
text: "Be incredibly careful installing skills from the internet - they may contain malicious code.",
|
|
31
32
|
},
|
|
32
33
|
];
|
|
34
|
+
const kDefaultProgress = {
|
|
35
|
+
stage: "creating_repo",
|
|
36
|
+
message: "Creating repo...",
|
|
37
|
+
};
|
|
38
|
+
const kProgressStepNumbers = {
|
|
39
|
+
creating_repo: 1,
|
|
40
|
+
running_openclaw_onboard: 2,
|
|
41
|
+
initial_git_push: 3,
|
|
42
|
+
starting_gateway: 4,
|
|
43
|
+
};
|
|
44
|
+
const kProgressPollIntervalMs = 500;
|
|
45
|
+
const kProgressDotsIntervalMs = 450;
|
|
46
|
+
const kProgressDotSlots = [1, 2, 3];
|
|
33
47
|
|
|
34
48
|
export const WelcomeSetupStep = ({ error, loading, onRetry, onBack }) => {
|
|
35
49
|
const [tipIndex, setTipIndex] = useState(0);
|
|
50
|
+
const [progress, setProgress] = useState(kDefaultProgress);
|
|
51
|
+
const [progressDotCount, setProgressDotCount] = useState(1);
|
|
36
52
|
|
|
37
53
|
useEffect(() => {
|
|
38
54
|
if (error || !loading) return;
|
|
@@ -42,6 +58,35 @@ export const WelcomeSetupStep = ({ error, loading, onRetry, onBack }) => {
|
|
|
42
58
|
return () => clearInterval(timer);
|
|
43
59
|
}, [error, loading]);
|
|
44
60
|
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (error || !loading) return;
|
|
63
|
+
let cancelled = false;
|
|
64
|
+
const refreshProgress = async () => {
|
|
65
|
+
try {
|
|
66
|
+
const progress = await fetchOnboardProgress();
|
|
67
|
+
if (!cancelled && progress?.message) {
|
|
68
|
+
setProgress(progress);
|
|
69
|
+
}
|
|
70
|
+
} catch {}
|
|
71
|
+
};
|
|
72
|
+
setProgress(kDefaultProgress);
|
|
73
|
+
refreshProgress();
|
|
74
|
+
const timer = setInterval(refreshProgress, kProgressPollIntervalMs);
|
|
75
|
+
return () => {
|
|
76
|
+
cancelled = true;
|
|
77
|
+
clearInterval(timer);
|
|
78
|
+
};
|
|
79
|
+
}, [error, loading]);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (error || !loading) return;
|
|
83
|
+
setProgressDotCount(1);
|
|
84
|
+
const timer = setInterval(() => {
|
|
85
|
+
setProgressDotCount((count) => (count % 3) + 1);
|
|
86
|
+
}, kProgressDotsIntervalMs);
|
|
87
|
+
return () => clearInterval(timer);
|
|
88
|
+
}, [error, loading]);
|
|
89
|
+
|
|
45
90
|
if (error) {
|
|
46
91
|
return html`
|
|
47
92
|
<div class="py-4 flex flex-col items-center text-center gap-3">
|
|
@@ -77,6 +122,8 @@ export const WelcomeSetupStep = ({ error, loading, onRetry, onBack }) => {
|
|
|
77
122
|
}
|
|
78
123
|
|
|
79
124
|
const currentTip = kSetupTips[tipIndex];
|
|
125
|
+
const progressStep = kProgressStepNumbers[progress.stage] || 1;
|
|
126
|
+
const progressLabel = (progress.message || kDefaultProgress.message).replace(/\.{1,3}$/, "");
|
|
80
127
|
|
|
81
128
|
return html`
|
|
82
129
|
<div class="relative min-h-[320px] pt-4 pb-20 flex">
|
|
@@ -84,10 +131,17 @@ export const WelcomeSetupStep = ({ error, loading, onRetry, onBack }) => {
|
|
|
84
131
|
class="flex-1 flex flex-col items-center justify-center text-center gap-4"
|
|
85
132
|
>
|
|
86
133
|
<${LoadingSpinner} className="h-8 w-8 text-body" />
|
|
87
|
-
<h3 class="text-lg font-semibold text-body">
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
134
|
+
<h3 class="text-lg font-semibold text-body">Initializing AlphaClaw...</h3>
|
|
135
|
+
<p class="text-sm text-fg-muted">
|
|
136
|
+
${progressStep} / 4: ${progressLabel}<span class="inline-flex" aria-hidden="true"
|
|
137
|
+
>${kProgressDotSlots.map(
|
|
138
|
+
(slot) => html`<span style=${{ visibility: slot <= progressDotCount ? "visible" : "hidden" }}
|
|
139
|
+
>.</span
|
|
140
|
+
>`,
|
|
141
|
+
)}</span
|
|
142
|
+
>
|
|
143
|
+
</p>
|
|
144
|
+
<p class="text-xs text-fg-muted">This could take up to 30 seconds</p>
|
|
91
145
|
</div>
|
|
92
146
|
<div
|
|
93
147
|
class="absolute bottom-3 left-3 right-3 bg-field border border-border rounded-lg px-3 py-2 text-xs text-fg-muted"
|
|
@@ -9,9 +9,11 @@ import {
|
|
|
9
9
|
import { useCachedFetch } from "../../hooks/use-cached-fetch.js";
|
|
10
10
|
import { usePolling } from "../../hooks/usePolling.js";
|
|
11
11
|
import {
|
|
12
|
-
getModelProvider,
|
|
13
12
|
getAuthProviderFromModelProvider,
|
|
14
13
|
getFeaturedModels,
|
|
14
|
+
getOnboardingModelProvider,
|
|
15
|
+
getOnboardingModels,
|
|
16
|
+
isDeprecatedOnboardingModelKey,
|
|
15
17
|
getVisibleAiFieldKeys,
|
|
16
18
|
kProviderAuthFields,
|
|
17
19
|
} from "../../lib/model-config.js";
|
|
@@ -167,7 +169,7 @@ export const useWelcome = ({ onComplete }) => {
|
|
|
167
169
|
};
|
|
168
170
|
|
|
169
171
|
const applyModelCatalog = useCallback((payload) => {
|
|
170
|
-
const list = getModelCatalogModels(payload);
|
|
172
|
+
const list = getOnboardingModels(getModelCatalogModels(payload));
|
|
171
173
|
if (!payload) return;
|
|
172
174
|
const isRefreshing = isModelCatalogRefreshing(payload);
|
|
173
175
|
const isFallbackRefresh =
|
|
@@ -181,11 +183,14 @@ export const useWelcome = ({ onComplete }) => {
|
|
|
181
183
|
: null
|
|
182
184
|
: "No models found",
|
|
183
185
|
);
|
|
186
|
+
const currentModelIsDeprecated = isDeprecatedOnboardingModelKey(
|
|
187
|
+
vals.MODEL_KEY,
|
|
188
|
+
);
|
|
184
189
|
const defaultModelKey = getInitialOnboardingModelKey({
|
|
185
190
|
catalog: list,
|
|
186
|
-
currentModelKey: vals.MODEL_KEY,
|
|
191
|
+
currentModelKey: currentModelIsDeprecated ? "" : vals.MODEL_KEY,
|
|
187
192
|
});
|
|
188
|
-
if (!vals.MODEL_KEY && defaultModelKey) {
|
|
193
|
+
if ((!vals.MODEL_KEY || currentModelIsDeprecated) && defaultModelKey) {
|
|
189
194
|
setVals((prev) => ({ ...prev, MODEL_KEY: defaultModelKey }));
|
|
190
195
|
}
|
|
191
196
|
}, [setVals, vals.MODEL_KEY]);
|
|
@@ -212,16 +217,18 @@ export const useWelcome = ({ onComplete }) => {
|
|
|
212
217
|
}, [modelsFetchState.error]);
|
|
213
218
|
|
|
214
219
|
const getValidationContext = (currentVals = {}) => {
|
|
215
|
-
const currentSelectedProvider =
|
|
216
|
-
|
|
217
|
-
|
|
220
|
+
const currentSelectedProvider = getOnboardingModelProvider({
|
|
221
|
+
modelKey: currentVals.MODEL_KEY,
|
|
222
|
+
models,
|
|
223
|
+
});
|
|
218
224
|
const currentSelectedAuthProvider =
|
|
219
225
|
getAuthProviderFromModelProvider(currentSelectedProvider);
|
|
220
226
|
const currentProviderAuthFields =
|
|
221
227
|
kProviderAuthFields[currentSelectedAuthProvider] || [];
|
|
222
228
|
const currentHasAi =
|
|
223
229
|
currentSelectedProvider === "openai-codex"
|
|
224
|
-
? !!codexStatus.connected
|
|
230
|
+
? !!codexStatus.connected ||
|
|
231
|
+
!!String(currentVals.OPENAI_API_KEY || "").trim()
|
|
225
232
|
: currentProviderAuthFields.some((field) =>
|
|
226
233
|
!!String(currentVals[field.key] || "").trim(),
|
|
227
234
|
);
|
package/lib/public/js/lib/api.js
CHANGED
|
@@ -300,7 +300,7 @@ export const updateDoctorCardStatus = async ({ cardId, status }) => {
|
|
|
300
300
|
|
|
301
301
|
export const sendDoctorCardFix = async ({
|
|
302
302
|
cardId,
|
|
303
|
-
|
|
303
|
+
sessionKey = "",
|
|
304
304
|
replyChannel = "",
|
|
305
305
|
replyTo = "",
|
|
306
306
|
prompt = "",
|
|
@@ -311,7 +311,7 @@ export const sendDoctorCardFix = async ({
|
|
|
311
311
|
method: "POST",
|
|
312
312
|
headers: { "Content-Type": "application/json" },
|
|
313
313
|
body: JSON.stringify({
|
|
314
|
-
|
|
314
|
+
sessionKey: String(sessionKey || ""),
|
|
315
315
|
replyChannel: String(replyChannel || ""),
|
|
316
316
|
replyTo: String(replyTo || ""),
|
|
317
317
|
prompt: String(prompt || ""),
|
|
@@ -818,6 +818,11 @@ export async function fetchOnboardStatus() {
|
|
|
818
818
|
return res.json();
|
|
819
819
|
}
|
|
820
820
|
|
|
821
|
+
export async function fetchOnboardProgress() {
|
|
822
|
+
const res = await authFetch("/api/onboard/progress");
|
|
823
|
+
return res.json();
|
|
824
|
+
}
|
|
825
|
+
|
|
821
826
|
export async function runOnboard(vars, modelKey, { importMode = false } = {}) {
|
|
822
827
|
const res = await authFetch("/api/onboard", {
|
|
823
828
|
method: "POST",
|
|
@@ -25,13 +25,6 @@ export const kFeaturedModelDefs = [
|
|
|
25
25
|
label: "Sonnet 4.6",
|
|
26
26
|
preferredKeys: ["anthropic/claude-sonnet-4-6"],
|
|
27
27
|
},
|
|
28
|
-
{
|
|
29
|
-
label: "Codex 5.3",
|
|
30
|
-
preferredKeys: [
|
|
31
|
-
"openai/gpt-5.3-codex",
|
|
32
|
-
"openai-codex/gpt-5.3-codex",
|
|
33
|
-
],
|
|
34
|
-
},
|
|
35
28
|
{
|
|
36
29
|
label: "GPT-5.5",
|
|
37
30
|
preferredKeys: ["openai/gpt-5.5", "openai-codex/gpt-5.5"],
|
|
@@ -42,6 +35,36 @@ export const kFeaturedModelDefs = [
|
|
|
42
35
|
},
|
|
43
36
|
];
|
|
44
37
|
|
|
38
|
+
const kDeprecatedOnboardingModelKeys = new Set([
|
|
39
|
+
"openai/gpt-5.3-codex",
|
|
40
|
+
"openai-codex/gpt-5.3-codex",
|
|
41
|
+
]);
|
|
42
|
+
const kCanonicalCodexOauthModelKeys = new Set([
|
|
43
|
+
"openai/gpt-5.4-mini",
|
|
44
|
+
"openai/gpt-5.5",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
export const isDeprecatedOnboardingModelKey = (modelKey) =>
|
|
48
|
+
kDeprecatedOnboardingModelKeys.has(String(modelKey || "").trim());
|
|
49
|
+
|
|
50
|
+
export const getOnboardingModels = (models = []) =>
|
|
51
|
+
models.filter(
|
|
52
|
+
(model) => !isDeprecatedOnboardingModelKey(model?.key),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
export const getOnboardingModelProvider = ({ modelKey, models = [] } = {}) => {
|
|
56
|
+
const normalizedKey = String(modelKey || "").trim();
|
|
57
|
+
const model = models.find((candidate) => candidate?.key === normalizedKey);
|
|
58
|
+
if (
|
|
59
|
+
getModelProvider(normalizedKey) === "openai-codex" ||
|
|
60
|
+
kCanonicalCodexOauthModelKeys.has(normalizedKey) ||
|
|
61
|
+
model?.agentRuntime?.id === "codex"
|
|
62
|
+
) {
|
|
63
|
+
return "openai-codex";
|
|
64
|
+
}
|
|
65
|
+
return getModelProvider(normalizedKey);
|
|
66
|
+
};
|
|
67
|
+
|
|
45
68
|
export const kAlwaysAvailableModelDefs = [
|
|
46
69
|
{
|
|
47
70
|
key: "openai/gpt-5.4-mini",
|
|
@@ -345,7 +368,6 @@ export const kFeatureDefs = [
|
|
|
345
368
|
];
|
|
346
369
|
|
|
347
370
|
export const getVisibleAiFieldKeys = (provider) => {
|
|
348
|
-
if (provider === "openai-codex") return new Set();
|
|
349
371
|
const authProvider = getAuthProviderFromModelProvider(provider);
|
|
350
372
|
const fields = kProviderAuthFields[authProvider] || [];
|
|
351
373
|
return new Set(fields.map((field) => field.key));
|
|
@@ -42,6 +42,7 @@ const buildPriorityCounts = (cards = []) => ({
|
|
|
42
42
|
|
|
43
43
|
const buildStatusCounts = (cards = []) => ({
|
|
44
44
|
open: cards.filter((card) => card.status === kDoctorCardStatus.open).length,
|
|
45
|
+
working: cards.filter((card) => card.status === kDoctorCardStatus.working).length,
|
|
45
46
|
dismissed: cards.filter((card) => card.status === kDoctorCardStatus.dismissed).length,
|
|
46
47
|
fixed: cards.filter((card) => card.status === kDoctorCardStatus.fixed).length,
|
|
47
48
|
});
|
|
@@ -138,9 +139,10 @@ const listDoctorCards = ({ runId } = {}) => {
|
|
|
138
139
|
${hasRunFilter ? "WHERE c.run_id = $run_id" : ""}
|
|
139
140
|
ORDER BY
|
|
140
141
|
CASE c.status
|
|
141
|
-
WHEN '
|
|
142
|
-
WHEN '
|
|
143
|
-
|
|
142
|
+
WHEN 'working' THEN 0
|
|
143
|
+
WHEN 'open' THEN 1
|
|
144
|
+
WHEN 'dismissed' THEN 2
|
|
145
|
+
ELSE 3
|
|
144
146
|
END ASC,
|
|
145
147
|
CASE c.priority
|
|
146
148
|
WHEN 'P0' THEN 0
|
|
@@ -177,14 +179,15 @@ const toRunModel = (row) => {
|
|
|
177
179
|
};
|
|
178
180
|
};
|
|
179
181
|
|
|
180
|
-
const initDoctorDb = ({ rootDir }) => {
|
|
182
|
+
const initDoctorDb = ({ rootDir, markInterruptedRuns = true }) => {
|
|
181
183
|
closeDoctorDb();
|
|
182
184
|
const dbDir = path.join(rootDir, "db");
|
|
183
185
|
fs.mkdirSync(dbDir, { recursive: true });
|
|
184
186
|
const dbPath = path.join(dbDir, "doctor.db");
|
|
185
187
|
db = new DatabaseSync(dbPath);
|
|
188
|
+
db.exec("PRAGMA busy_timeout = 5000;");
|
|
186
189
|
createSchema(db);
|
|
187
|
-
markIncompleteRunsFailed();
|
|
190
|
+
if (markInterruptedRuns) markIncompleteRunsFailed();
|
|
188
191
|
return { path: dbPath };
|
|
189
192
|
};
|
|
190
193
|
|
|
@@ -507,12 +510,95 @@ const updateDoctorCardStatus = ({ id, status }) => {
|
|
|
507
510
|
UPDATE doctor_cards
|
|
508
511
|
SET
|
|
509
512
|
status = $status,
|
|
513
|
+
fix_run_id = NULL,
|
|
514
|
+
fix_token_hash = NULL,
|
|
515
|
+
fix_started_at = NULL,
|
|
516
|
+
fix_completed_at = CASE WHEN $status = $fixed_status
|
|
517
|
+
THEN strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
|
518
|
+
ELSE NULL
|
|
519
|
+
END,
|
|
510
520
|
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
|
511
521
|
WHERE id = $id
|
|
512
522
|
`)
|
|
513
523
|
.run({
|
|
514
524
|
$id: Number(id || 0),
|
|
515
525
|
$status: nextStatus,
|
|
526
|
+
$fixed_status: kDoctorCardStatus.fixed,
|
|
527
|
+
});
|
|
528
|
+
return Number(result.changes || 0) > 0 ? getDoctorCard(id) : null;
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const startDoctorCardFix = ({ id, runId, tokenHash }) => {
|
|
532
|
+
const database = ensureDb();
|
|
533
|
+
const result = database
|
|
534
|
+
.prepare(`
|
|
535
|
+
UPDATE doctor_cards
|
|
536
|
+
SET
|
|
537
|
+
status = $status,
|
|
538
|
+
fix_run_id = $run_id,
|
|
539
|
+
fix_token_hash = $token_hash,
|
|
540
|
+
fix_started_at = strftime('%Y-%m-%dT%H:%M:%fZ','now'),
|
|
541
|
+
fix_completed_at = NULL,
|
|
542
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
|
543
|
+
WHERE id = $id
|
|
544
|
+
AND status = $open_status
|
|
545
|
+
`)
|
|
546
|
+
.run({
|
|
547
|
+
$id: Number(id || 0),
|
|
548
|
+
$status: kDoctorCardStatus.working,
|
|
549
|
+
$open_status: kDoctorCardStatus.open,
|
|
550
|
+
$run_id: String(runId || ""),
|
|
551
|
+
$token_hash: String(tokenHash || ""),
|
|
552
|
+
});
|
|
553
|
+
return Number(result.changes || 0) > 0 ? getDoctorCard(id) : null;
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const cancelDoctorCardFix = ({ id, runId }) => {
|
|
557
|
+
const database = ensureDb();
|
|
558
|
+
const result = database
|
|
559
|
+
.prepare(`
|
|
560
|
+
UPDATE doctor_cards
|
|
561
|
+
SET
|
|
562
|
+
status = $status,
|
|
563
|
+
fix_run_id = NULL,
|
|
564
|
+
fix_token_hash = NULL,
|
|
565
|
+
fix_started_at = NULL,
|
|
566
|
+
fix_completed_at = NULL,
|
|
567
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
|
568
|
+
WHERE id = $id
|
|
569
|
+
AND status = $working_status
|
|
570
|
+
AND fix_run_id = $run_id
|
|
571
|
+
`)
|
|
572
|
+
.run({
|
|
573
|
+
$id: Number(id || 0),
|
|
574
|
+
$status: kDoctorCardStatus.open,
|
|
575
|
+
$working_status: kDoctorCardStatus.working,
|
|
576
|
+
$run_id: String(runId || ""),
|
|
577
|
+
});
|
|
578
|
+
return Number(result.changes || 0) > 0 ? getDoctorCard(id) : null;
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const completeDoctorCardFix = ({ id, runId, tokenHash }) => {
|
|
582
|
+
const database = ensureDb();
|
|
583
|
+
const result = database
|
|
584
|
+
.prepare(`
|
|
585
|
+
UPDATE doctor_cards
|
|
586
|
+
SET
|
|
587
|
+
status = $status,
|
|
588
|
+
fix_token_hash = NULL,
|
|
589
|
+
fix_completed_at = strftime('%Y-%m-%dT%H:%M:%fZ','now'),
|
|
590
|
+
updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
|
591
|
+
WHERE id = $id
|
|
592
|
+
AND status = $working_status
|
|
593
|
+
AND fix_run_id = $run_id
|
|
594
|
+
AND fix_token_hash = $token_hash
|
|
595
|
+
`)
|
|
596
|
+
.run({
|
|
597
|
+
$id: Number(id || 0),
|
|
598
|
+
$status: kDoctorCardStatus.fixed,
|
|
599
|
+
$working_status: kDoctorCardStatus.working,
|
|
600
|
+
$run_id: String(runId || ""),
|
|
601
|
+
$token_hash: String(tokenHash || ""),
|
|
516
602
|
});
|
|
517
603
|
return Number(result.changes || 0) > 0 ? getDoctorCard(id) : null;
|
|
518
604
|
};
|
|
@@ -535,4 +621,7 @@ module.exports = {
|
|
|
535
621
|
getDoctorCardsByRunId: getCardsByRunId,
|
|
536
622
|
getDoctorCard,
|
|
537
623
|
updateDoctorCardStatus,
|
|
624
|
+
startDoctorCardFix,
|
|
625
|
+
cancelDoctorCardFix,
|
|
626
|
+
completeDoctorCardFix,
|
|
538
627
|
};
|
|
@@ -58,6 +58,10 @@ const createSchema = (database) => {
|
|
|
58
58
|
ensureColumn(database, "doctor_runs", "workspace_fingerprint", "TEXT");
|
|
59
59
|
ensureColumn(database, "doctor_runs", "workspace_manifest_json", "TEXT");
|
|
60
60
|
ensureColumn(database, "doctor_runs", "reused_from_run_id", "INTEGER");
|
|
61
|
+
ensureColumn(database, "doctor_cards", "fix_run_id", "TEXT");
|
|
62
|
+
ensureColumn(database, "doctor_cards", "fix_token_hash", "TEXT");
|
|
63
|
+
ensureColumn(database, "doctor_cards", "fix_started_at", "TEXT");
|
|
64
|
+
ensureColumn(database, "doctor_cards", "fix_completed_at", "TEXT");
|
|
61
65
|
database.exec(`
|
|
62
66
|
CREATE INDEX IF NOT EXISTS idx_doctor_cards_run_id
|
|
63
67
|
ON doctor_cards(run_id, created_at DESC);
|