@chrysb/alphaclaw 0.9.27 → 0.9.28
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 +641 -632
- 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/lib/api.js +2 -2
- 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/routes/doctor.js +5 -2
- 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;
|
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 || ""),
|
|
@@ -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);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
1
2
|
const fs = require("fs");
|
|
2
3
|
const path = require("path");
|
|
3
4
|
const {
|
|
@@ -5,9 +6,11 @@ const {
|
|
|
5
6
|
buildBootstrapTruncationCards,
|
|
6
7
|
} = require("./bootstrap-context");
|
|
7
8
|
const { buildDoctorPrompt } = require("./prompt");
|
|
9
|
+
const { hashDoctorFixToken } = require("./fix-completion");
|
|
8
10
|
const { normalizeDoctorResult } = require("./normalize");
|
|
9
11
|
const { calculateWorkspaceDelta, computeWorkspaceSnapshot } = require("./workspace-fingerprint");
|
|
10
12
|
const {
|
|
13
|
+
kDoctorCardStatus,
|
|
11
14
|
kDoctorEngine,
|
|
12
15
|
kDoctorMeaningfulChangeScoreThreshold,
|
|
13
16
|
kDoctorPromptVersion,
|
|
@@ -17,6 +20,35 @@ const {
|
|
|
17
20
|
} = require("./constants");
|
|
18
21
|
|
|
19
22
|
const kMaxSnippetLines = 20;
|
|
23
|
+
const kDoctorFixDispatchTimeoutMs = 30000;
|
|
24
|
+
|
|
25
|
+
const buildDoctorFixCompletionInstructions = ({
|
|
26
|
+
cardId,
|
|
27
|
+
runId,
|
|
28
|
+
token,
|
|
29
|
+
alphaclawRootDir,
|
|
30
|
+
}) => {
|
|
31
|
+
const completionCommand = [
|
|
32
|
+
"alphaclaw",
|
|
33
|
+
"--root-dir",
|
|
34
|
+
shellEscapeArg(alphaclawRootDir),
|
|
35
|
+
"doctor finding complete",
|
|
36
|
+
"--id",
|
|
37
|
+
shellEscapeArg(String(Number(cardId || 0))),
|
|
38
|
+
"--run",
|
|
39
|
+
shellEscapeArg(runId),
|
|
40
|
+
"--token",
|
|
41
|
+
shellEscapeArg(token),
|
|
42
|
+
].join(" ");
|
|
43
|
+
return [
|
|
44
|
+
"AlphaClaw completion callback:",
|
|
45
|
+
"After you have successfully applied and verified the requested fix, run this command exactly:",
|
|
46
|
+
"```sh",
|
|
47
|
+
completionCommand,
|
|
48
|
+
"```",
|
|
49
|
+
"Do not call the completion callback if the fix was not applied or verification failed. Explain the problem to the user instead.",
|
|
50
|
+
].join("\n");
|
|
51
|
+
};
|
|
20
52
|
|
|
21
53
|
const shellEscapeArg = (value) => {
|
|
22
54
|
const safeValue = String(value || "");
|
|
@@ -91,8 +123,11 @@ const createDoctorService = ({
|
|
|
91
123
|
getDoctorCardsByRunId,
|
|
92
124
|
getDoctorCard,
|
|
93
125
|
updateDoctorCardStatus,
|
|
126
|
+
startDoctorCardFix,
|
|
127
|
+
cancelDoctorCardFix,
|
|
94
128
|
workspaceRoot,
|
|
95
129
|
managedRoot,
|
|
130
|
+
alphaclawRootDir = process.env.ALPHACLAW_ROOT_DIR || "~/.alphaclaw",
|
|
96
131
|
protectedPaths = [],
|
|
97
132
|
lockedPaths = [],
|
|
98
133
|
}) => {
|
|
@@ -133,7 +168,13 @@ const createDoctorService = ({
|
|
|
133
168
|
};
|
|
134
169
|
|
|
135
170
|
const cloneRunCards = ({ sourceRunId, targetRunId }) => {
|
|
136
|
-
const sourceCards = getDoctorCardsByRunId(sourceRunId)
|
|
171
|
+
const sourceCards = getDoctorCardsByRunId(sourceRunId).map((card) => ({
|
|
172
|
+
...card,
|
|
173
|
+
status:
|
|
174
|
+
card.status === kDoctorCardStatus.working
|
|
175
|
+
? kDoctorCardStatus.open
|
|
176
|
+
: card.status,
|
|
177
|
+
}));
|
|
137
178
|
insertDoctorCards({
|
|
138
179
|
runId: targetRunId,
|
|
139
180
|
cards: sourceCards,
|
|
@@ -385,7 +426,7 @@ const createDoctorService = ({
|
|
|
385
426
|
|
|
386
427
|
const requestCardFix = async ({
|
|
387
428
|
cardId,
|
|
388
|
-
|
|
429
|
+
sessionKey = "",
|
|
389
430
|
replyChannel = "",
|
|
390
431
|
replyTo = "",
|
|
391
432
|
prompt = "",
|
|
@@ -394,28 +435,65 @@ const createDoctorService = ({
|
|
|
394
435
|
if (!card) throw new Error("Doctor card not found");
|
|
395
436
|
const resolvedPrompt = String(prompt || card.fixPrompt || "").trim();
|
|
396
437
|
if (!resolvedPrompt) throw new Error("Doctor card does not include a fix prompt");
|
|
397
|
-
|
|
398
|
-
|
|
438
|
+
const trimmedSessionKey = String(sessionKey || "").trim();
|
|
439
|
+
if (!trimmedSessionKey) throw new Error("Doctor fix request requires a session key");
|
|
399
440
|
const trimmedReplyChannel = String(replyChannel || "").trim();
|
|
400
441
|
const trimmedReplyTo = String(replyTo || "").trim();
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
442
|
+
const runId = `doctor-fix-${Number(card.id || cardId)}-${crypto.randomUUID()}`;
|
|
443
|
+
const callbackToken = crypto.randomBytes(32).toString("hex");
|
|
444
|
+
const workingCard = startDoctorCardFix({
|
|
445
|
+
id: card.id,
|
|
446
|
+
runId,
|
|
447
|
+
tokenHash: hashDoctorFixToken(callbackToken),
|
|
448
|
+
});
|
|
449
|
+
if (!workingCard) {
|
|
450
|
+
const currentCard = getDoctorCard(card.id);
|
|
451
|
+
if (currentCard?.status === kDoctorCardStatus.working) {
|
|
452
|
+
throw new Error("Doctor fix already in progress");
|
|
453
|
+
}
|
|
454
|
+
throw new Error("Doctor finding must be open before requesting a fix");
|
|
407
455
|
}
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
456
|
+
const completionInstructions = buildDoctorFixCompletionInstructions({
|
|
457
|
+
cardId: card.id,
|
|
458
|
+
runId,
|
|
459
|
+
token: callbackToken,
|
|
460
|
+
alphaclawRootDir,
|
|
411
461
|
});
|
|
462
|
+
const gatewayParams = {
|
|
463
|
+
idempotencyKey: runId,
|
|
464
|
+
message: `${resolvedPrompt}\n\n${completionInstructions}`,
|
|
465
|
+
sessionKey: trimmedSessionKey,
|
|
466
|
+
};
|
|
467
|
+
if (trimmedReplyChannel && trimmedReplyTo) {
|
|
468
|
+
gatewayParams.deliver = true;
|
|
469
|
+
gatewayParams.replyChannel = trimmedReplyChannel;
|
|
470
|
+
gatewayParams.replyTo = trimmedReplyTo;
|
|
471
|
+
}
|
|
472
|
+
let result = null;
|
|
473
|
+
try {
|
|
474
|
+
result = await clawCmd(
|
|
475
|
+
`gateway call agent --json --timeout ${kDoctorFixDispatchTimeoutMs} --params ${shellEscapeArg(
|
|
476
|
+
JSON.stringify(gatewayParams),
|
|
477
|
+
)}`,
|
|
478
|
+
{
|
|
479
|
+
quiet: true,
|
|
480
|
+
timeoutMs: kDoctorFixDispatchTimeoutMs,
|
|
481
|
+
},
|
|
482
|
+
);
|
|
483
|
+
} catch (error) {
|
|
484
|
+
cancelDoctorCardFix({ id: card.id, runId });
|
|
485
|
+
throw error;
|
|
486
|
+
}
|
|
412
487
|
if (!result?.ok) {
|
|
488
|
+
cancelDoctorCardFix({ id: card.id, runId });
|
|
413
489
|
throw new Error(result?.stderr || "Could not send Doctor fix request");
|
|
414
490
|
}
|
|
415
491
|
return {
|
|
416
492
|
ok: true,
|
|
493
|
+
queued: true,
|
|
494
|
+
runId,
|
|
417
495
|
stdout: result.stdout || "",
|
|
418
|
-
card,
|
|
496
|
+
card: workingCard,
|
|
419
497
|
};
|
|
420
498
|
};
|
|
421
499
|
|
|
@@ -443,6 +521,7 @@ const createDoctorService = ({
|
|
|
443
521
|
};
|
|
444
522
|
|
|
445
523
|
module.exports = {
|
|
524
|
+
buildDoctorFixCompletionInstructions,
|
|
446
525
|
buildDoctorIdempotencyKey,
|
|
447
526
|
buildDoctorSessionKey,
|
|
448
527
|
buildDoctorSessionId,
|
|
@@ -106,13 +106,16 @@ const registerDoctorRoutes = ({ app, requireAuth, doctorService }) => {
|
|
|
106
106
|
try {
|
|
107
107
|
const result = await doctorService.requestCardFix({
|
|
108
108
|
cardId: req.params.id,
|
|
109
|
-
|
|
109
|
+
sessionKey: req.body?.sessionKey,
|
|
110
110
|
replyChannel: req.body?.replyChannel,
|
|
111
111
|
replyTo: req.body?.replyTo,
|
|
112
112
|
prompt: req.body?.prompt,
|
|
113
113
|
});
|
|
114
|
-
return res.json(result);
|
|
114
|
+
return res.status(result?.queued ? 202 : 200).json(result);
|
|
115
115
|
} catch (error) {
|
|
116
|
+
if (/already in progress|must be open/i.test(error.message || "")) {
|
|
117
|
+
return res.status(409).json({ ok: false, error: error.message });
|
|
118
|
+
}
|
|
116
119
|
if (/not found/i.test(error.message || "")) {
|
|
117
120
|
return res.status(404).json({ ok: false, error: error.message });
|
|
118
121
|
}
|
package/lib/server.js
CHANGED
|
@@ -64,6 +64,8 @@ const {
|
|
|
64
64
|
getDoctorCardsByRunId,
|
|
65
65
|
getDoctorCard,
|
|
66
66
|
updateDoctorCardStatus,
|
|
67
|
+
startDoctorCardFix,
|
|
68
|
+
cancelDoctorCardFix,
|
|
67
69
|
} = require("./server/db/doctor");
|
|
68
70
|
const {
|
|
69
71
|
initAuthDb,
|
|
@@ -306,8 +308,11 @@ const doctorService = createDoctorService({
|
|
|
306
308
|
getDoctorCardsByRunId,
|
|
307
309
|
getDoctorCard,
|
|
308
310
|
updateDoctorCardStatus,
|
|
311
|
+
startDoctorCardFix,
|
|
312
|
+
cancelDoctorCardFix,
|
|
309
313
|
workspaceRoot: constants.WORKSPACE_DIR,
|
|
310
314
|
managedRoot: constants.OPENCLAW_DIR,
|
|
315
|
+
alphaclawRootDir: constants.kRootDir,
|
|
311
316
|
protectedPaths: Array.from(constants.kProtectedBrowsePaths || []),
|
|
312
317
|
lockedPaths: Array.from(constants.kLockedBrowsePaths || []),
|
|
313
318
|
});
|