@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.
@@ -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="Ask agent to fix"
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, updateDoctorCardStatus } from "../../lib/api.js";
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
- sessionId: selectedSession?.sessionId || "",
20
+ sessionKey: selectedSessionKey,
21
21
  replyChannel: selectedSession?.replyChannel || "",
22
22
  replyTo: selectedSession?.replyTo || "",
23
23
  prompt: message,
24
24
  });
25
- try {
26
- await updateDoctorCardStatus({ cardId: card.id, status: "fixed" });
27
- showToast(
28
- "Doctor fix request sent and finding marked fixed",
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="Sending..."
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 (ctx.selectedProvider === "openai-codex" && ctx.codexLoading) {
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
- Initializing OpenClaw...
89
- </h3>
90
- <p class="text-sm text-fg-muted">This could take 10-15 seconds</p>
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 = getModelProvider(
216
- String(currentVals.MODEL_KEY || "").trim(),
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
  );
@@ -300,7 +300,7 @@ export const updateDoctorCardStatus = async ({ cardId, status }) => {
300
300
 
301
301
  export const sendDoctorCardFix = async ({
302
302
  cardId,
303
- sessionId = "",
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
- sessionId: String(sessionId || ""),
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 'open' THEN 0
142
- WHEN 'dismissed' THEN 1
143
- ELSE 2
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);
@@ -6,6 +6,7 @@ const kDoctorRunStatus = {
6
6
  };
7
7
  const kDoctorCardStatus = {
8
8
  open: "open",
9
+ working: "working",
9
10
  dismissed: "dismissed",
10
11
  fixed: "fixed",
11
12
  };
@@ -0,0 +1,6 @@
1
+ const crypto = require("crypto");
2
+
3
+ const hashDoctorFixToken = (token) =>
4
+ crypto.createHash("sha256").update(String(token || "")).digest("hex");
5
+
6
+ module.exports = { hashDoctorFixToken };