@chrysb/alphaclaw 0.4.4 → 0.4.6-beta.0

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 (37) hide show
  1. package/README.md +21 -18
  2. package/lib/public/css/theme.css +29 -0
  3. package/lib/public/js/app.js +41 -2
  4. package/lib/public/js/components/badge.js +4 -0
  5. package/lib/public/js/components/doctor/findings-list.js +191 -0
  6. package/lib/public/js/components/doctor/fix-card-modal.js +144 -0
  7. package/lib/public/js/components/doctor/general-warning.js +37 -0
  8. package/lib/public/js/components/doctor/helpers.js +169 -0
  9. package/lib/public/js/components/doctor/index.js +536 -0
  10. package/lib/public/js/components/doctor/summary-cards.js +24 -0
  11. package/lib/public/js/lib/api.js +79 -0
  12. package/lib/server/commands.js +8 -4
  13. package/lib/server/constants.js +22 -26
  14. package/lib/server/db/doctor/index.js +529 -0
  15. package/lib/server/db/doctor/schema.js +69 -0
  16. package/lib/server/doctor/constants.js +43 -0
  17. package/lib/server/doctor/normalize.js +214 -0
  18. package/lib/server/doctor/prompt.js +89 -0
  19. package/lib/server/doctor/service.js +392 -0
  20. package/lib/server/doctor/workspace-fingerprint.js +126 -0
  21. package/lib/server/gmail-push.js +102 -6
  22. package/lib/server/gmail-watch.js +5 -20
  23. package/lib/server/helpers.js +5 -21
  24. package/lib/server/routes/doctor.js +123 -0
  25. package/lib/server/routes/google.js +2 -10
  26. package/lib/server/routes/system.js +7 -1
  27. package/lib/server/routes/telegram.js +3 -14
  28. package/lib/server/routes/usage.js +1 -5
  29. package/lib/server/routes/webhooks.js +2 -6
  30. package/lib/server/utils/boolean.js +22 -0
  31. package/lib/server/utils/json.js +77 -0
  32. package/lib/server/utils/network.js +5 -0
  33. package/lib/server/utils/number.js +8 -0
  34. package/lib/server/utils/shell.js +16 -0
  35. package/lib/server/webhook-middleware.js +1 -2
  36. package/lib/server.js +42 -0
  37. package/package.json +1 -1
@@ -0,0 +1,529 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { DatabaseSync } = require("node:sqlite");
4
+ const { createSchema } = require("./schema");
5
+ const {
6
+ kDoctorCardStatus,
7
+ kDoctorDefaultRunsLimit,
8
+ kDoctorMaxRunsLimit,
9
+ kDoctorPriority,
10
+ kDoctorRunStatus,
11
+ } = require("../../doctor/constants");
12
+
13
+ let db = null;
14
+ const kDoctorInitialBaselineMetaKey = "initial_workspace_baseline";
15
+
16
+ const ensureDb = () => {
17
+ if (!db) throw new Error("Doctor DB not initialized");
18
+ return db;
19
+ };
20
+
21
+ const parseJsonText = (value, fallbackValue) => {
22
+ if (typeof value !== "string" || !value) return fallbackValue;
23
+ try {
24
+ return JSON.parse(value);
25
+ } catch {
26
+ return fallbackValue;
27
+ }
28
+ };
29
+
30
+ const buildPriorityCounts = (cards = []) => ({
31
+ P0: cards.filter((card) => card.priority === kDoctorPriority.P0).length,
32
+ P1: cards.filter((card) => card.priority === kDoctorPriority.P1).length,
33
+ P2: cards.filter((card) => card.priority === kDoctorPriority.P2).length,
34
+ });
35
+
36
+ const buildStatusCounts = (cards = []) => ({
37
+ open: cards.filter((card) => card.status === kDoctorCardStatus.open).length,
38
+ dismissed: cards.filter((card) => card.status === kDoctorCardStatus.dismissed).length,
39
+ fixed: cards.filter((card) => card.status === kDoctorCardStatus.fixed).length,
40
+ });
41
+
42
+ const toCardModel = (row) => {
43
+ if (!row) return null;
44
+ return {
45
+ id: Number(row.id || 0),
46
+ runId: Number(row.run_id || 0),
47
+ createdAt: row.created_at || null,
48
+ updatedAt: row.updated_at || null,
49
+ priority: row.priority || kDoctorPriority.P2,
50
+ category: row.category || "workspace",
51
+ title: row.title || "",
52
+ summary: row.summary || "",
53
+ recommendation: row.recommendation || "",
54
+ evidence: parseJsonText(row.evidence_json, []),
55
+ targetPaths: parseJsonText(row.target_paths_json, []),
56
+ fixPrompt: row.fix_prompt || "",
57
+ status: row.status || kDoctorCardStatus.open,
58
+ };
59
+ };
60
+
61
+ const attachRunCounts = (run, cards = []) =>
62
+ run
63
+ ? {
64
+ ...run,
65
+ cardCount: cards.length,
66
+ priorityCounts: buildPriorityCounts(cards),
67
+ statusCounts: buildStatusCounts(cards),
68
+ }
69
+ : null;
70
+
71
+ const getCardsByRunId = (runId) => {
72
+ const database = ensureDb();
73
+ const rows = database
74
+ .prepare(`
75
+ SELECT
76
+ id,
77
+ run_id,
78
+ created_at,
79
+ updated_at,
80
+ priority,
81
+ category,
82
+ title,
83
+ summary,
84
+ recommendation,
85
+ evidence_json,
86
+ target_paths_json,
87
+ fix_prompt,
88
+ status
89
+ FROM doctor_cards
90
+ WHERE run_id = $run_id
91
+ ORDER BY
92
+ CASE priority
93
+ WHEN 'P0' THEN 0
94
+ WHEN 'P1' THEN 1
95
+ ELSE 2
96
+ END ASC,
97
+ created_at DESC
98
+ `)
99
+ .all({ $run_id: Number(runId || 0) });
100
+ return rows.map(toCardModel);
101
+ };
102
+
103
+ const listDoctorCards = ({ runId } = {}) => {
104
+ const database = ensureDb();
105
+ const hasRunFilter =
106
+ runId !== undefined &&
107
+ runId !== null &&
108
+ String(runId || "").trim() !== "" &&
109
+ String(runId || "").trim().toLowerCase() !== "all";
110
+ const rows = database
111
+ .prepare(`
112
+ SELECT
113
+ c.id,
114
+ c.run_id,
115
+ c.created_at,
116
+ c.updated_at,
117
+ c.priority,
118
+ c.category,
119
+ c.title,
120
+ c.summary,
121
+ c.recommendation,
122
+ c.evidence_json,
123
+ c.target_paths_json,
124
+ c.fix_prompt,
125
+ c.status,
126
+ r.started_at AS run_started_at,
127
+ r.completed_at AS run_completed_at,
128
+ r.status AS run_status
129
+ FROM doctor_cards c
130
+ INNER JOIN doctor_runs r ON r.id = c.run_id
131
+ ${hasRunFilter ? "WHERE c.run_id = $run_id" : ""}
132
+ ORDER BY
133
+ CASE c.status
134
+ WHEN 'open' THEN 0
135
+ WHEN 'dismissed' THEN 1
136
+ ELSE 2
137
+ END ASC,
138
+ CASE c.priority
139
+ WHEN 'P0' THEN 0
140
+ WHEN 'P1' THEN 1
141
+ ELSE 2
142
+ END ASC,
143
+ c.created_at DESC
144
+ `)
145
+ .all(hasRunFilter ? { $run_id: Number(runId || 0) } : {});
146
+ return rows.map((row) => ({
147
+ ...toCardModel(row),
148
+ runStartedAt: row.run_started_at || null,
149
+ runCompletedAt: row.run_completed_at || null,
150
+ runStatus: row.run_status || kDoctorRunStatus.failed,
151
+ }));
152
+ };
153
+
154
+ const toRunModel = (row) => {
155
+ if (!row) return null;
156
+ return {
157
+ id: Number(row.id || 0),
158
+ startedAt: row.started_at || null,
159
+ completedAt: row.completed_at || null,
160
+ status: row.status || kDoctorRunStatus.failed,
161
+ engine: row.engine || "",
162
+ workspaceRoot: row.workspace_root || "",
163
+ workspaceFingerprint: row.workspace_fingerprint || "",
164
+ workspaceManifest: parseJsonText(row.workspace_manifest_json, null),
165
+ promptVersion: row.prompt_version || "",
166
+ summary: row.summary || "",
167
+ rawResult: parseJsonText(row.raw_result_json, null),
168
+ error: row.error || "",
169
+ reusedFromRunId: Number(row.reused_from_run_id || 0),
170
+ };
171
+ };
172
+
173
+ const initDoctorDb = ({ rootDir }) => {
174
+ const dbDir = path.join(rootDir, "db");
175
+ fs.mkdirSync(dbDir, { recursive: true });
176
+ const dbPath = path.join(dbDir, "doctor.db");
177
+ db = new DatabaseSync(dbPath);
178
+ createSchema(db);
179
+ markIncompleteRunsFailed();
180
+ return { path: dbPath };
181
+ };
182
+
183
+ const getDoctorMeta = (key) => {
184
+ const database = ensureDb();
185
+ const row = database
186
+ .prepare(`
187
+ SELECT
188
+ key,
189
+ value_json,
190
+ updated_at
191
+ FROM doctor_meta
192
+ WHERE key = $key
193
+ LIMIT 1
194
+ `)
195
+ .get({ $key: String(key || "") });
196
+ if (!row) return null;
197
+ return {
198
+ key: row.key || "",
199
+ value: parseJsonText(row.value_json, null),
200
+ updatedAt: row.updated_at || null,
201
+ };
202
+ };
203
+
204
+ const setDoctorMeta = ({ key, value = null }) => {
205
+ const database = ensureDb();
206
+ database
207
+ .prepare(`
208
+ INSERT INTO doctor_meta (
209
+ key,
210
+ value_json,
211
+ updated_at
212
+ ) VALUES (
213
+ $key,
214
+ $value_json,
215
+ strftime('%Y-%m-%dT%H:%M:%fZ','now')
216
+ )
217
+ ON CONFLICT(key) DO UPDATE SET
218
+ value_json = excluded.value_json,
219
+ updated_at = excluded.updated_at
220
+ `)
221
+ .run({
222
+ $key: String(key || ""),
223
+ $value_json: value == null ? null : JSON.stringify(value),
224
+ });
225
+ return getDoctorMeta(key);
226
+ };
227
+
228
+ const getInitialWorkspaceBaseline = () => getDoctorMeta(kDoctorInitialBaselineMetaKey)?.value || null;
229
+
230
+ const setInitialWorkspaceBaseline = (baseline) =>
231
+ setDoctorMeta({
232
+ key: kDoctorInitialBaselineMetaKey,
233
+ value: baseline,
234
+ })?.value || null;
235
+
236
+ const markIncompleteRunsFailed = (errorMessage = "Doctor run interrupted before completion") => {
237
+ const database = ensureDb();
238
+ const result = database
239
+ .prepare(`
240
+ UPDATE doctor_runs
241
+ SET
242
+ status = $status,
243
+ completed_at = COALESCE(completed_at, strftime('%Y-%m-%dT%H:%M:%fZ','now')),
244
+ error = COALESCE(NULLIF(error, ''), $error)
245
+ WHERE status = $running_status
246
+ `)
247
+ .run({
248
+ $status: kDoctorRunStatus.failed,
249
+ $running_status: kDoctorRunStatus.running,
250
+ $error: String(errorMessage || ""),
251
+ });
252
+ return Number(result.changes || 0);
253
+ };
254
+
255
+ const createDoctorRun = ({
256
+ status = kDoctorRunStatus.running,
257
+ engine,
258
+ workspaceRoot,
259
+ workspaceFingerprint = "",
260
+ workspaceManifest = null,
261
+ promptVersion,
262
+ reusedFromRunId = 0,
263
+ }) => {
264
+ const database = ensureDb();
265
+ const result = database
266
+ .prepare(`
267
+ INSERT INTO doctor_runs (
268
+ status,
269
+ engine,
270
+ workspace_root,
271
+ workspace_fingerprint,
272
+ workspace_manifest_json,
273
+ prompt_version,
274
+ reused_from_run_id
275
+ ) VALUES (
276
+ $status,
277
+ $engine,
278
+ $workspace_root,
279
+ $workspace_fingerprint,
280
+ $workspace_manifest_json,
281
+ $prompt_version,
282
+ $reused_from_run_id
283
+ )
284
+ `)
285
+ .run({
286
+ $status: String(status || kDoctorRunStatus.running),
287
+ $engine: String(engine || ""),
288
+ $workspace_root: String(workspaceRoot || ""),
289
+ $workspace_fingerprint: String(workspaceFingerprint || ""),
290
+ $workspace_manifest_json: workspaceManifest == null ? null : JSON.stringify(workspaceManifest),
291
+ $prompt_version: String(promptVersion || ""),
292
+ $reused_from_run_id: Number(reusedFromRunId || 0),
293
+ });
294
+ return Number(result.lastInsertRowid || 0);
295
+ };
296
+
297
+ const completeDoctorRun = ({
298
+ id,
299
+ status,
300
+ summary = "",
301
+ rawResult = null,
302
+ error = "",
303
+ }) => {
304
+ const database = ensureDb();
305
+ const result = database
306
+ .prepare(`
307
+ UPDATE doctor_runs
308
+ SET
309
+ completed_at = strftime('%Y-%m-%dT%H:%M:%fZ','now'),
310
+ status = $status,
311
+ summary = $summary,
312
+ raw_result_json = $raw_result_json,
313
+ error = $error
314
+ WHERE id = $id
315
+ `)
316
+ .run({
317
+ $id: Number(id || 0),
318
+ $status: String(status || kDoctorRunStatus.failed),
319
+ $summary: String(summary || ""),
320
+ $raw_result_json: rawResult == null ? null : JSON.stringify(rawResult),
321
+ $error: String(error || ""),
322
+ });
323
+ return Number(result.changes || 0);
324
+ };
325
+
326
+ const insertDoctorCards = ({ runId, cards = [] }) => {
327
+ const database = ensureDb();
328
+ database.exec("BEGIN");
329
+ try {
330
+ const stmt = database.prepare(`
331
+ INSERT INTO doctor_cards (
332
+ run_id,
333
+ priority,
334
+ category,
335
+ title,
336
+ summary,
337
+ recommendation,
338
+ evidence_json,
339
+ target_paths_json,
340
+ fix_prompt,
341
+ status
342
+ ) VALUES (
343
+ $run_id,
344
+ $priority,
345
+ $category,
346
+ $title,
347
+ $summary,
348
+ $recommendation,
349
+ $evidence_json,
350
+ $target_paths_json,
351
+ $fix_prompt,
352
+ $status
353
+ )
354
+ `);
355
+ for (const card of cards) {
356
+ stmt.run({
357
+ $run_id: Number(runId || 0),
358
+ $priority: String(card?.priority || kDoctorPriority.P2),
359
+ $category: String(card?.category || "workspace"),
360
+ $title: String(card?.title || ""),
361
+ $summary: String(card?.summary || ""),
362
+ $recommendation: String(card?.recommendation || ""),
363
+ $evidence_json: JSON.stringify(card?.evidence || []),
364
+ $target_paths_json: JSON.stringify(card?.targetPaths || []),
365
+ $fix_prompt: String(card?.fixPrompt || ""),
366
+ $status: String(card?.status || kDoctorCardStatus.open),
367
+ });
368
+ }
369
+ database.exec("COMMIT");
370
+ } catch (error) {
371
+ database.exec("ROLLBACK");
372
+ throw error;
373
+ }
374
+ };
375
+
376
+ const getDoctorRun = (id) => {
377
+ const database = ensureDb();
378
+ const row = database
379
+ .prepare(`
380
+ SELECT
381
+ id,
382
+ started_at,
383
+ completed_at,
384
+ status,
385
+ engine,
386
+ workspace_root,
387
+ workspace_fingerprint,
388
+ workspace_manifest_json,
389
+ prompt_version,
390
+ summary,
391
+ raw_result_json,
392
+ error,
393
+ reused_from_run_id
394
+ FROM doctor_runs
395
+ WHERE id = $id
396
+ LIMIT 1
397
+ `)
398
+ .get({ $id: Number(id || 0) });
399
+ const run = toRunModel(row);
400
+ if (!run) return null;
401
+ return attachRunCounts(run, getCardsByRunId(run.id));
402
+ };
403
+
404
+ const getLatestDoctorRun = () => {
405
+ const database = ensureDb();
406
+ const row = database
407
+ .prepare(`
408
+ SELECT
409
+ id,
410
+ started_at,
411
+ completed_at,
412
+ status,
413
+ engine,
414
+ workspace_root,
415
+ workspace_fingerprint,
416
+ workspace_manifest_json,
417
+ prompt_version,
418
+ summary,
419
+ raw_result_json,
420
+ error,
421
+ reused_from_run_id
422
+ FROM doctor_runs
423
+ ORDER BY started_at DESC
424
+ LIMIT 1
425
+ `)
426
+ .get();
427
+ const run = toRunModel(row);
428
+ if (!run) return null;
429
+ return attachRunCounts(run, getCardsByRunId(run.id));
430
+ };
431
+
432
+ const listDoctorRuns = ({ limit = kDoctorDefaultRunsLimit } = {}) => {
433
+ const database = ensureDb();
434
+ const safeLimit = Math.max(
435
+ 1,
436
+ Math.min(Number.parseInt(String(limit || kDoctorDefaultRunsLimit), 10) || kDoctorDefaultRunsLimit, kDoctorMaxRunsLimit),
437
+ );
438
+ const rows = database
439
+ .prepare(`
440
+ SELECT
441
+ id,
442
+ started_at,
443
+ completed_at,
444
+ status,
445
+ engine,
446
+ workspace_root,
447
+ workspace_fingerprint,
448
+ workspace_manifest_json,
449
+ prompt_version,
450
+ summary,
451
+ raw_result_json,
452
+ error,
453
+ reused_from_run_id
454
+ FROM doctor_runs
455
+ ORDER BY started_at DESC
456
+ LIMIT $limit
457
+ `)
458
+ .all({ $limit: safeLimit });
459
+ return rows.map((row) => {
460
+ const run = toRunModel(row);
461
+ return attachRunCounts(run, getCardsByRunId(run.id));
462
+ });
463
+ };
464
+
465
+ const getDoctorCard = (id) => {
466
+ const database = ensureDb();
467
+ const row = database
468
+ .prepare(`
469
+ SELECT
470
+ id,
471
+ run_id,
472
+ created_at,
473
+ updated_at,
474
+ priority,
475
+ category,
476
+ title,
477
+ summary,
478
+ recommendation,
479
+ evidence_json,
480
+ target_paths_json,
481
+ fix_prompt,
482
+ status
483
+ FROM doctor_cards
484
+ WHERE id = $id
485
+ LIMIT 1
486
+ `)
487
+ .get({ $id: Number(id || 0) });
488
+ return toCardModel(row);
489
+ };
490
+
491
+ const updateDoctorCardStatus = ({ id, status }) => {
492
+ const database = ensureDb();
493
+ const nextStatus =
494
+ status === kDoctorCardStatus.fixed || status === kDoctorCardStatus.dismissed
495
+ ? status
496
+ : kDoctorCardStatus.open;
497
+ const result = database
498
+ .prepare(`
499
+ UPDATE doctor_cards
500
+ SET
501
+ status = $status,
502
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
503
+ WHERE id = $id
504
+ `)
505
+ .run({
506
+ $id: Number(id || 0),
507
+ $status: nextStatus,
508
+ });
509
+ return Number(result.changes || 0) > 0 ? getDoctorCard(id) : null;
510
+ };
511
+
512
+ module.exports = {
513
+ initDoctorDb,
514
+ markIncompleteRunsFailed,
515
+ getDoctorMeta,
516
+ setDoctorMeta,
517
+ getInitialWorkspaceBaseline,
518
+ setInitialWorkspaceBaseline,
519
+ createDoctorRun,
520
+ completeDoctorRun,
521
+ insertDoctorCards,
522
+ getDoctorRun,
523
+ getLatestDoctorRun,
524
+ listDoctorRuns,
525
+ listDoctorCards,
526
+ getDoctorCardsByRunId: getCardsByRunId,
527
+ getDoctorCard,
528
+ updateDoctorCardStatus,
529
+ };
@@ -0,0 +1,69 @@
1
+ const hasColumn = (database, tableName, columnName) => {
2
+ const rows = database.prepare(`PRAGMA table_info(${tableName})`).all();
3
+ return rows.some((row) => row.name === columnName);
4
+ };
5
+
6
+ const ensureColumn = (database, tableName, columnName, definition) => {
7
+ if (hasColumn(database, tableName, columnName)) return;
8
+ database.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition};`);
9
+ };
10
+
11
+ const createSchema = (database) => {
12
+ database.exec(`
13
+ CREATE TABLE IF NOT EXISTS doctor_runs (
14
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15
+ started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
16
+ completed_at TEXT,
17
+ status TEXT NOT NULL,
18
+ engine TEXT NOT NULL,
19
+ workspace_root TEXT NOT NULL,
20
+ workspace_fingerprint TEXT,
21
+ workspace_manifest_json TEXT,
22
+ prompt_version TEXT NOT NULL,
23
+ summary TEXT,
24
+ raw_result_json TEXT,
25
+ error TEXT,
26
+ reused_from_run_id INTEGER
27
+ );
28
+ `);
29
+ database.exec(`
30
+ CREATE TABLE IF NOT EXISTS doctor_meta (
31
+ key TEXT PRIMARY KEY,
32
+ value_json TEXT,
33
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
34
+ );
35
+ `);
36
+ database.exec(`
37
+ CREATE TABLE IF NOT EXISTS doctor_cards (
38
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
39
+ run_id INTEGER NOT NULL,
40
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
41
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
42
+ priority TEXT NOT NULL,
43
+ category TEXT NOT NULL,
44
+ title TEXT NOT NULL,
45
+ summary TEXT,
46
+ recommendation TEXT NOT NULL,
47
+ evidence_json TEXT,
48
+ target_paths_json TEXT,
49
+ fix_prompt TEXT NOT NULL,
50
+ status TEXT NOT NULL,
51
+ FOREIGN KEY (run_id) REFERENCES doctor_runs(id) ON DELETE CASCADE
52
+ );
53
+ `);
54
+ database.exec(`
55
+ CREATE INDEX IF NOT EXISTS idx_doctor_runs_started_at
56
+ ON doctor_runs(started_at DESC);
57
+ `);
58
+ ensureColumn(database, "doctor_runs", "workspace_fingerprint", "TEXT");
59
+ ensureColumn(database, "doctor_runs", "workspace_manifest_json", "TEXT");
60
+ ensureColumn(database, "doctor_runs", "reused_from_run_id", "INTEGER");
61
+ database.exec(`
62
+ CREATE INDEX IF NOT EXISTS idx_doctor_cards_run_id
63
+ ON doctor_cards(run_id, created_at DESC);
64
+ `);
65
+ };
66
+
67
+ module.exports = {
68
+ createSchema,
69
+ };
@@ -0,0 +1,43 @@
1
+ const kDoctorPromptVersion = "doctor-v1";
2
+ const kDoctorRunStatus = {
3
+ running: "running",
4
+ completed: "completed",
5
+ failed: "failed",
6
+ };
7
+ const kDoctorCardStatus = {
8
+ open: "open",
9
+ dismissed: "dismissed",
10
+ fixed: "fixed",
11
+ };
12
+ const kDoctorPriority = {
13
+ P0: "P0",
14
+ P1: "P1",
15
+ P2: "P2",
16
+ };
17
+ const kDoctorEngine = {
18
+ gatewayAgent: "gateway_agent",
19
+ acpRuntime: "acp_runtime",
20
+ agentMessageFallback: "agent_message_fallback",
21
+ manualImport: "manual_import",
22
+ deterministicReuse: "deterministic_reuse",
23
+ };
24
+ const kDoctorStaleThresholdMs = 7 * 24 * 60 * 60 * 1000;
25
+ const kDoctorMeaningfulChangeScoreThreshold = 4;
26
+ const kDoctorRunTimeoutMs = 10 * 60 * 1000;
27
+ const kDoctorDefaultRunsLimit = 10;
28
+ const kDoctorMaxRunsLimit = 50;
29
+ const kDoctorMaxCardsPerRun = 12;
30
+
31
+ module.exports = {
32
+ kDoctorPromptVersion,
33
+ kDoctorRunStatus,
34
+ kDoctorCardStatus,
35
+ kDoctorPriority,
36
+ kDoctorEngine,
37
+ kDoctorStaleThresholdMs,
38
+ kDoctorMeaningfulChangeScoreThreshold,
39
+ kDoctorRunTimeoutMs,
40
+ kDoctorDefaultRunsLimit,
41
+ kDoctorMaxRunsLimit,
42
+ kDoctorMaxCardsPerRun,
43
+ };