@chrysb/alphaclaw 0.4.6-beta.7 → 0.4.6-beta.8
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/lib/public/js/components/doctor/helpers.js +71 -5
- package/lib/public/js/components/doctor/index.js +89 -28
- package/lib/server/doctor/bootstrap-context.js +191 -0
- package/lib/server/doctor/prompt.js +20 -4
- package/lib/server/doctor/service.js +18 -4
- package/lib/server.js +1 -1
- package/package.json +1 -1
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
export const getDoctorPriorityTone = (priority = "") => {
|
|
2
|
-
const normalized = String(priority || "")
|
|
2
|
+
const normalized = String(priority || "")
|
|
3
|
+
.trim()
|
|
4
|
+
.toUpperCase();
|
|
3
5
|
if (normalized === "P0") return "danger";
|
|
4
6
|
if (normalized === "P1") return "warning";
|
|
5
7
|
return "neutral";
|
|
6
8
|
};
|
|
7
9
|
|
|
8
10
|
export const getDoctorStatusTone = (status = "") => {
|
|
9
|
-
const normalized = String(status || "")
|
|
11
|
+
const normalized = String(status || "")
|
|
12
|
+
.trim()
|
|
13
|
+
.toLowerCase();
|
|
10
14
|
if (normalized === "fixed") return "success";
|
|
11
15
|
if (normalized === "dismissed") return "neutral";
|
|
12
16
|
return "warning";
|
|
@@ -35,7 +39,9 @@ export const formatDoctorCategory = (category = "") => {
|
|
|
35
39
|
export const buildDoctorPriorityCounts = (cards = []) =>
|
|
36
40
|
cards.reduce(
|
|
37
41
|
(totals, card) => {
|
|
38
|
-
const priority = String(card?.priority || "")
|
|
42
|
+
const priority = String(card?.priority || "")
|
|
43
|
+
.trim()
|
|
44
|
+
.toUpperCase();
|
|
39
45
|
if (priority === "P0" || priority === "P1" || priority === "P2") {
|
|
40
46
|
totals[priority] += 1;
|
|
41
47
|
}
|
|
@@ -47,7 +53,9 @@ export const buildDoctorPriorityCounts = (cards = []) =>
|
|
|
47
53
|
export const groupDoctorCardsByStatus = (cards = []) =>
|
|
48
54
|
cards.reduce(
|
|
49
55
|
(groups, card) => {
|
|
50
|
-
const status = String(card?.status || "open")
|
|
56
|
+
const status = String(card?.status || "open")
|
|
57
|
+
.trim()
|
|
58
|
+
.toLowerCase();
|
|
51
59
|
if (status === "fixed") {
|
|
52
60
|
groups.fixed.push(card);
|
|
53
61
|
return groups;
|
|
@@ -74,13 +82,71 @@ export const shouldShowDoctorWarning = (
|
|
|
74
82
|
|
|
75
83
|
export const getDoctorWarningMessage = (doctorStatus = null) => {
|
|
76
84
|
if (!doctorStatus) return "";
|
|
77
|
-
const changedFilesCount = Number(
|
|
85
|
+
const changedFilesCount = Number(
|
|
86
|
+
doctorStatus.changeSummary?.changedFilesCount || 0,
|
|
87
|
+
);
|
|
78
88
|
if (changedFilesCount > 0) {
|
|
79
89
|
return `Drift Doctor has not been run in the last week and ${changedFilesCount} file${changedFilesCount === 1 ? "" : "s"} changed since the last review.`;
|
|
80
90
|
}
|
|
81
91
|
return "Doctor has not been run in the last week.";
|
|
82
92
|
};
|
|
83
93
|
|
|
94
|
+
export const formatDoctorCharCount = (value = 0) =>
|
|
95
|
+
`${Number(value || 0).toLocaleString()} chars`;
|
|
96
|
+
|
|
97
|
+
const isManagedBootstrapContextPath = (filePath = "") =>
|
|
98
|
+
String(filePath || "").startsWith("hooks/bootstrap/");
|
|
99
|
+
|
|
100
|
+
export const getDoctorBootstrapTruncationItems = (doctorStatus = null) => {
|
|
101
|
+
const bootstrapContext = doctorStatus?.bootstrapContext;
|
|
102
|
+
const truncatedFiles = (bootstrapContext?.activeTruncatedFiles || []).filter(
|
|
103
|
+
(file) => !isManagedBootstrapContextPath(file?.path),
|
|
104
|
+
);
|
|
105
|
+
const nearLimitFiles = (bootstrapContext?.activeNearLimitFiles || []).filter(
|
|
106
|
+
(file) => !isManagedBootstrapContextPath(file?.path),
|
|
107
|
+
);
|
|
108
|
+
return [
|
|
109
|
+
...truncatedFiles.map((file) => ({
|
|
110
|
+
path: file.path,
|
|
111
|
+
size: formatDoctorCharCount(file.rawChars),
|
|
112
|
+
statusText: `-${Number(
|
|
113
|
+
Math.max(
|
|
114
|
+
0,
|
|
115
|
+
Number(file.rawChars || 0) - Number(file.injectedChars || 0),
|
|
116
|
+
),
|
|
117
|
+
).toLocaleString()} cut`,
|
|
118
|
+
statusTone: "danger",
|
|
119
|
+
})),
|
|
120
|
+
...nearLimitFiles.map((file) => ({
|
|
121
|
+
path: file.path,
|
|
122
|
+
size: formatDoctorCharCount(file.rawChars),
|
|
123
|
+
statusText: "Near limit",
|
|
124
|
+
statusTone: "warning",
|
|
125
|
+
})),
|
|
126
|
+
];
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const hasDoctorBootstrapWarnings = (doctorStatus = null) =>
|
|
130
|
+
getDoctorBootstrapTruncationItems(doctorStatus).length > 0;
|
|
131
|
+
|
|
132
|
+
export const getDoctorBootstrapWarningTitle = (doctorStatus = null) => {
|
|
133
|
+
const items = getDoctorBootstrapTruncationItems(doctorStatus);
|
|
134
|
+
if (!items.length) return "";
|
|
135
|
+
const hasTruncatedItems = items.some((item) => item.statusTone === "danger");
|
|
136
|
+
const hasNearLimitItems = items.some((item) => item.statusTone === "warning");
|
|
137
|
+
if (hasTruncatedItems && hasNearLimitItems) {
|
|
138
|
+
return "Some of your main files are being truncated or nearing the limit:";
|
|
139
|
+
}
|
|
140
|
+
if (hasNearLimitItems) {
|
|
141
|
+
return items.length === 1
|
|
142
|
+
? "One of your main files is nearing the limit:"
|
|
143
|
+
: "Some of your main files are nearing the limit:";
|
|
144
|
+
}
|
|
145
|
+
return items.length === 1
|
|
146
|
+
? "One of your main files is being truncated:"
|
|
147
|
+
: "Some of your main files are being truncated:";
|
|
148
|
+
};
|
|
149
|
+
|
|
84
150
|
export const getDoctorChangeLabel = (changeSummary = null) => {
|
|
85
151
|
const changedFilesCount = Number(changeSummary?.changedFilesCount || 0);
|
|
86
152
|
if (changedFilesCount === 0) return "No changes since last run";
|
|
@@ -20,8 +20,11 @@ import { DoctorFixCardModal } from "./fix-card-modal.js";
|
|
|
20
20
|
import {
|
|
21
21
|
buildDoctorRunMarkers,
|
|
22
22
|
buildDoctorStatusFilterOptions,
|
|
23
|
+
getDoctorBootstrapTruncationItems,
|
|
24
|
+
getDoctorBootstrapWarningTitle,
|
|
23
25
|
getDoctorChangeLabel,
|
|
24
26
|
getDoctorRunPillDetail,
|
|
27
|
+
hasDoctorBootstrapWarnings,
|
|
25
28
|
shouldShowDoctorWarning,
|
|
26
29
|
} from "./helpers.js";
|
|
27
30
|
|
|
@@ -182,6 +185,18 @@ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
|
|
|
182
185
|
() => shouldShowDoctorWarning(doctorStatus, 0),
|
|
183
186
|
[doctorStatus],
|
|
184
187
|
);
|
|
188
|
+
const showBootstrapTruncationBanner = useMemo(
|
|
189
|
+
() => hasDoctorBootstrapWarnings(doctorStatus),
|
|
190
|
+
[doctorStatus],
|
|
191
|
+
);
|
|
192
|
+
const bootstrapTruncationMessage = useMemo(
|
|
193
|
+
() => getDoctorBootstrapWarningTitle(doctorStatus),
|
|
194
|
+
[doctorStatus],
|
|
195
|
+
);
|
|
196
|
+
const bootstrapTruncationItems = useMemo(
|
|
197
|
+
() => getDoctorBootstrapTruncationItems(doctorStatus),
|
|
198
|
+
[doctorStatus],
|
|
199
|
+
);
|
|
185
200
|
const hasCompletedDoctorRun = !!doctorStatus?.lastRunAt;
|
|
186
201
|
const hasRuns = runs.length > 0;
|
|
187
202
|
const hasLoadedRuns = runsPoll.data !== null || runsPoll.error !== null;
|
|
@@ -312,29 +327,76 @@ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
|
|
|
312
327
|
: null}
|
|
313
328
|
${!showInitialLoadingState && hasRuns
|
|
314
329
|
? html`
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
330
|
+
<div class="space-y-3">
|
|
331
|
+
<${DoctorSummaryCards} cards=${openCards} />
|
|
332
|
+
<div class="space-y-3">
|
|
333
|
+
${hasCompletedDoctorRun
|
|
334
|
+
? html`
|
|
335
|
+
<div
|
|
336
|
+
class="bg-surface border border-border rounded-xl p-4 flex flex-wrap items-center justify-between gap-3"
|
|
337
|
+
>
|
|
338
|
+
<span class="text-xs text-gray-500">
|
|
339
|
+
Last run ·${" "}
|
|
340
|
+
<span class="text-gray-300">
|
|
341
|
+
${formatLocaleDateTime(doctorStatus?.lastRunAt, {
|
|
342
|
+
fallback: "Never",
|
|
343
|
+
})}
|
|
344
|
+
</span>
|
|
328
345
|
</span>
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
</
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
346
|
+
<span class="text-xs text-gray-500">
|
|
347
|
+
${changeLabel}
|
|
348
|
+
</span>
|
|
349
|
+
</div>
|
|
350
|
+
${showBootstrapTruncationBanner
|
|
351
|
+
? html`
|
|
352
|
+
<div
|
|
353
|
+
class="bg-surface border border-border rounded-xl p-4 space-y-3"
|
|
354
|
+
>
|
|
355
|
+
<div class="text-xs text-gray-400">
|
|
356
|
+
⚠️ ${bootstrapTruncationMessage}
|
|
357
|
+
</div>
|
|
358
|
+
<div class="space-y-2">
|
|
359
|
+
${bootstrapTruncationItems.map(
|
|
360
|
+
(item) => html`
|
|
361
|
+
<div
|
|
362
|
+
class="flex items-center justify-between gap-3 text-xs"
|
|
363
|
+
>
|
|
364
|
+
<button
|
|
365
|
+
type="button"
|
|
366
|
+
class="font-mono text-gray-200 ac-tip-link hover:underline text-left cursor-pointer"
|
|
367
|
+
onClick=${() => onOpenFile(String(item.path || ""))}
|
|
368
|
+
>
|
|
369
|
+
${item.path}
|
|
370
|
+
</button>
|
|
371
|
+
<span
|
|
372
|
+
class="flex items-center gap-3 whitespace-nowrap"
|
|
373
|
+
>
|
|
374
|
+
<span class="text-gray-500">
|
|
375
|
+
${item.size}
|
|
376
|
+
</span>
|
|
377
|
+
<span
|
|
378
|
+
class=${item.statusTone === "warning"
|
|
379
|
+
? "text-yellow-300"
|
|
380
|
+
: "text-red-300"}
|
|
381
|
+
>
|
|
382
|
+
${item.statusText}
|
|
383
|
+
</span>
|
|
384
|
+
</span>
|
|
385
|
+
</div>
|
|
386
|
+
`,
|
|
387
|
+
)}
|
|
388
|
+
</div>
|
|
389
|
+
<div class="border-t border-border"></div>
|
|
390
|
+
<p class="text-xs text-gray-500 leading-5">
|
|
391
|
+
Truncated files become partially hidden from
|
|
392
|
+
your agent and could cause drift.
|
|
393
|
+
</p>
|
|
394
|
+
</div>
|
|
395
|
+
`
|
|
396
|
+
: null}
|
|
397
|
+
`
|
|
398
|
+
: null}
|
|
399
|
+
${showDoctorStaleBanner
|
|
338
400
|
? html`
|
|
339
401
|
<div
|
|
340
402
|
class="text-xs text-yellow-300 bg-yellow-500/10 border border-yellow-500/35 rounded-lg px-3 py-2"
|
|
@@ -344,8 +406,8 @@ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
|
|
|
344
406
|
changed.
|
|
345
407
|
</div>
|
|
346
408
|
`
|
|
347
|
-
: null
|
|
348
|
-
|
|
409
|
+
: null}
|
|
410
|
+
</div>
|
|
349
411
|
</div>
|
|
350
412
|
`
|
|
351
413
|
: null}
|
|
@@ -461,9 +523,7 @@ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
|
|
|
461
523
|
${selectedRunIsInProgress
|
|
462
524
|
? html`
|
|
463
525
|
<div class="ac-surface-inset rounded-xl p-4">
|
|
464
|
-
<div
|
|
465
|
-
class="text-xs leading-5 text-gray-400"
|
|
466
|
-
>
|
|
526
|
+
<div class="text-xs leading-5 text-gray-400">
|
|
467
527
|
<span
|
|
468
528
|
>Run in progress. Findings will appear when analysis
|
|
469
529
|
completes.</span
|
|
@@ -479,7 +539,8 @@ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
|
|
|
479
539
|
onAskAgentFix=${setFixCard}
|
|
480
540
|
onUpdateStatus=${handleUpdateStatus}
|
|
481
541
|
onOpenFile=${onOpenFile}
|
|
482
|
-
changedPaths=${doctorStatus?.changeSummary?.changedPaths ||
|
|
542
|
+
changedPaths=${doctorStatus?.changeSummary?.changedPaths ||
|
|
543
|
+
[]}
|
|
483
544
|
showRunMeta=${selectedRunFilter === "all"}
|
|
484
545
|
hideEmptyState=${selectedRunIsInProgress}
|
|
485
546
|
/>
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const kDoctorBootstrapMaxChars = 20000;
|
|
5
|
+
const kDoctorBootstrapTotalMaxChars = 150000;
|
|
6
|
+
const kDoctorBootstrapNearLimitRatio = 0.9;
|
|
7
|
+
const kDoctorContextTruncationGuidance =
|
|
8
|
+
"OpenClaw trims oversized injected files by keeping the first 70%, keeping the last 20%, and cutting the middle 10% without a warning.";
|
|
9
|
+
|
|
10
|
+
const kDoctorRootContextFiles = [
|
|
11
|
+
{ path: "AGENTS.md", injectMode: "always" },
|
|
12
|
+
{ path: "SOUL.md", injectMode: "always" },
|
|
13
|
+
{ path: "TOOLS.md", injectMode: "always" },
|
|
14
|
+
{ path: "IDENTITY.md", injectMode: "always" },
|
|
15
|
+
{ path: "USER.md", injectMode: "always" },
|
|
16
|
+
{ path: "HEARTBEAT.md", injectMode: "always" },
|
|
17
|
+
{ path: "BOOTSTRAP.md", injectMode: "first_run_only" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const kDoctorBootstrapExtraFiles = [
|
|
21
|
+
{ path: "hooks/bootstrap/AGENTS.md", injectMode: "always" },
|
|
22
|
+
{ path: "hooks/bootstrap/TOOLS.md", injectMode: "always" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const kDoctorBootstrapContextFiles = [...kDoctorRootContextFiles, ...kDoctorBootstrapExtraFiles];
|
|
26
|
+
|
|
27
|
+
const readWorkspaceFileChars = (workspaceRoot, relativePath) => {
|
|
28
|
+
const fullPath = path.join(workspaceRoot, relativePath);
|
|
29
|
+
try {
|
|
30
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
31
|
+
return {
|
|
32
|
+
exists: true,
|
|
33
|
+
chars: content.length,
|
|
34
|
+
};
|
|
35
|
+
} catch {
|
|
36
|
+
return {
|
|
37
|
+
exists: false,
|
|
38
|
+
chars: 0,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const analyzeBootstrapContext = ({
|
|
44
|
+
workspaceRoot = "",
|
|
45
|
+
bootstrapMaxChars = kDoctorBootstrapMaxChars,
|
|
46
|
+
bootstrapTotalMaxChars = kDoctorBootstrapTotalMaxChars,
|
|
47
|
+
} = {}) => {
|
|
48
|
+
const files = kDoctorBootstrapContextFiles.map((spec) => {
|
|
49
|
+
const fileState = readWorkspaceFileChars(workspaceRoot, spec.path);
|
|
50
|
+
const rawChars = fileState.chars;
|
|
51
|
+
const fileLimitChars = Math.min(rawChars, bootstrapMaxChars);
|
|
52
|
+
const nearFileLimit = rawChars > 0 && rawChars >= Math.floor(bootstrapMaxChars * kDoctorBootstrapNearLimitRatio);
|
|
53
|
+
return {
|
|
54
|
+
...spec,
|
|
55
|
+
exists: fileState.exists,
|
|
56
|
+
rawChars,
|
|
57
|
+
fileLimitChars,
|
|
58
|
+
injectedChars: 0,
|
|
59
|
+
truncatedByFileLimit: rawChars > bootstrapMaxChars,
|
|
60
|
+
truncatedByTotalLimit: false,
|
|
61
|
+
truncated: rawChars > bootstrapMaxChars,
|
|
62
|
+
nearFileLimit: nearFileLimit && rawChars <= bootstrapMaxChars,
|
|
63
|
+
active: spec.injectMode === "always",
|
|
64
|
+
reason: rawChars > bootstrapMaxChars ? "file_limit" : "",
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
let injectedTotalChars = 0;
|
|
69
|
+
for (const file of files) {
|
|
70
|
+
if (!file.active || !file.exists) continue;
|
|
71
|
+
const remainingChars = Math.max(0, bootstrapTotalMaxChars - injectedTotalChars);
|
|
72
|
+
file.injectedChars = Math.min(file.fileLimitChars, remainingChars);
|
|
73
|
+
file.truncatedByTotalLimit = file.fileLimitChars > file.injectedChars;
|
|
74
|
+
file.truncated = file.truncatedByFileLimit || file.truncatedByTotalLimit;
|
|
75
|
+
if (file.truncatedByFileLimit && file.truncatedByTotalLimit) {
|
|
76
|
+
file.reason = "file_and_total_limit";
|
|
77
|
+
} else if (file.truncatedByFileLimit) {
|
|
78
|
+
file.reason = "file_limit";
|
|
79
|
+
} else if (file.truncatedByTotalLimit) {
|
|
80
|
+
file.reason = "total_limit";
|
|
81
|
+
}
|
|
82
|
+
injectedTotalChars += file.injectedChars;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const activeFiles = files.filter((file) => file.active && file.exists);
|
|
86
|
+
const activeTruncatedFiles = activeFiles.filter((file) => file.truncated);
|
|
87
|
+
const activeNearLimitFiles = activeFiles.filter((file) => file.nearFileLimit && !file.truncated);
|
|
88
|
+
const inactiveTruncatedFiles = files.filter((file) => !file.active && file.exists && file.truncated);
|
|
89
|
+
const hasTotalLimitTruncation = activeTruncatedFiles.some(
|
|
90
|
+
(file) => file.reason === "total_limit" || file.reason === "file_and_total_limit",
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
bootstrapMaxChars,
|
|
95
|
+
bootstrapTotalMaxChars,
|
|
96
|
+
truncationGuidance: kDoctorContextTruncationGuidance,
|
|
97
|
+
files,
|
|
98
|
+
activeFiles,
|
|
99
|
+
activeRawChars: activeFiles.reduce((sum, file) => sum + file.rawChars, 0),
|
|
100
|
+
activeInjectedChars: activeFiles.reduce((sum, file) => sum + file.injectedChars, 0),
|
|
101
|
+
hasActiveTruncation: activeTruncatedFiles.length > 0,
|
|
102
|
+
hasActiveNearLimitFiles: activeNearLimitFiles.length > 0,
|
|
103
|
+
hasActiveWarnings: activeTruncatedFiles.length > 0 || activeNearLimitFiles.length > 0,
|
|
104
|
+
hasAnyTruncation: activeTruncatedFiles.length > 0 || inactiveTruncatedFiles.length > 0,
|
|
105
|
+
activeTruncatedFiles,
|
|
106
|
+
activeNearLimitFiles,
|
|
107
|
+
inactiveTruncatedFiles,
|
|
108
|
+
hasTotalLimitTruncation,
|
|
109
|
+
totalLimitReached: injectedTotalChars >= bootstrapTotalMaxChars,
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const formatChars = (value = 0) => `${Number(value || 0).toLocaleString()} chars`;
|
|
114
|
+
|
|
115
|
+
const buildBootstrapTruncationCards = (bootstrapContext = null) => {
|
|
116
|
+
if (!bootstrapContext?.hasActiveTruncation) return [];
|
|
117
|
+
|
|
118
|
+
const cards = bootstrapContext.activeTruncatedFiles
|
|
119
|
+
.filter((file) => file.reason === "file_limit")
|
|
120
|
+
.map((file) => ({
|
|
121
|
+
priority: "P0",
|
|
122
|
+
category: "project context",
|
|
123
|
+
title: `${file.path} is being truncated in Project Context`,
|
|
124
|
+
summary:
|
|
125
|
+
`${file.path} is ${formatChars(file.rawChars)}, above the per-file Project Context limit ` +
|
|
126
|
+
`of ${formatChars(bootstrapContext.bootstrapMaxChars)}. The agent is not seeing the full file.`,
|
|
127
|
+
recommendation:
|
|
128
|
+
`Move the most important rules to the top of ${file.path}, shorten or split low-priority content, ` +
|
|
129
|
+
`and increase OpenClaw's bootstrap limits if this file legitimately needs more room. ` +
|
|
130
|
+
kDoctorContextTruncationGuidance,
|
|
131
|
+
evidence: [
|
|
132
|
+
{ type: "path", path: file.path },
|
|
133
|
+
{
|
|
134
|
+
type: "text",
|
|
135
|
+
text:
|
|
136
|
+
`Raw size: ${formatChars(file.rawChars)}. ` +
|
|
137
|
+
`Per-file limit: ${formatChars(bootstrapContext.bootstrapMaxChars)}.`,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
targetPaths: [{ path: file.path }],
|
|
141
|
+
fixPrompt:
|
|
142
|
+
`Reorganize ${file.path} so the most important instructions appear at the top and reduce unnecessary length. ` +
|
|
143
|
+
`Do not change unrelated behavior.`,
|
|
144
|
+
status: "open",
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
const totalLimitedFiles = bootstrapContext.activeTruncatedFiles.filter(
|
|
148
|
+
(file) => file.reason === "total_limit" || file.reason === "file_and_total_limit",
|
|
149
|
+
);
|
|
150
|
+
if (totalLimitedFiles.length > 0) {
|
|
151
|
+
cards.unshift({
|
|
152
|
+
priority: "P0",
|
|
153
|
+
category: "project context",
|
|
154
|
+
title: "Project Context total bootstrap limit is truncating injected files",
|
|
155
|
+
summary:
|
|
156
|
+
`Injected workspace guidance needs ${formatChars(bootstrapContext.activeRawChars)} raw across active ` +
|
|
157
|
+
`Project Context files, exceeding the total bootstrap budget of ` +
|
|
158
|
+
`${formatChars(bootstrapContext.bootstrapTotalMaxChars)}.`,
|
|
159
|
+
recommendation:
|
|
160
|
+
`Reduce total Project Context size across injected guidance files, keep critical instructions near the top, ` +
|
|
161
|
+
`and raise OpenClaw's total bootstrap budget if the workspace legitimately needs more injected guidance. ` +
|
|
162
|
+
kDoctorContextTruncationGuidance,
|
|
163
|
+
evidence: totalLimitedFiles.map((file) => ({
|
|
164
|
+
type: "text",
|
|
165
|
+
text:
|
|
166
|
+
`${file.path}: raw ${formatChars(file.rawChars)}, injected ${formatChars(file.injectedChars)} ` +
|
|
167
|
+
`before the total limit stopped more content from being included.`,
|
|
168
|
+
})),
|
|
169
|
+
targetPaths: totalLimitedFiles.map((file) => ({ path: file.path })),
|
|
170
|
+
fixPrompt:
|
|
171
|
+
`Reduce the combined size of the affected Project Context files and keep the most important instructions near the top. ` +
|
|
172
|
+
`Only edit the files listed in the finding.`,
|
|
173
|
+
status: "open",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return cards;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
module.exports = {
|
|
181
|
+
analyzeBootstrapContext,
|
|
182
|
+
buildBootstrapTruncationCards,
|
|
183
|
+
formatChars,
|
|
184
|
+
kDoctorBootstrapContextFiles,
|
|
185
|
+
kDoctorBootstrapExtraFiles,
|
|
186
|
+
kDoctorBootstrapMaxChars,
|
|
187
|
+
kDoctorBootstrapNearLimitRatio,
|
|
188
|
+
kDoctorBootstrapTotalMaxChars,
|
|
189
|
+
kDoctorContextTruncationGuidance,
|
|
190
|
+
kDoctorRootContextFiles,
|
|
191
|
+
};
|
|
@@ -1,6 +1,17 @@
|
|
|
1
|
+
const {
|
|
2
|
+
kDoctorBootstrapExtraFiles,
|
|
3
|
+
kDoctorBootstrapMaxChars,
|
|
4
|
+
kDoctorBootstrapTotalMaxChars,
|
|
5
|
+
kDoctorContextTruncationGuidance,
|
|
6
|
+
kDoctorRootContextFiles,
|
|
7
|
+
} = require("./bootstrap-context");
|
|
8
|
+
|
|
1
9
|
const renderList = (items = []) =>
|
|
2
10
|
items.length ? items.map((item) => `- ${item}`).join("\n") : "- (none)";
|
|
3
11
|
|
|
12
|
+
const renderContextFileList = (files = []) =>
|
|
13
|
+
files.map((file) => `\`${file.path}\``).join(", ");
|
|
14
|
+
|
|
4
15
|
const renderResolvedCards = (cards = []) => {
|
|
5
16
|
if (!cards.length) return "";
|
|
6
17
|
const lines = cards.map(
|
|
@@ -37,10 +48,15 @@ Important:
|
|
|
37
48
|
- Return ONLY valid JSON. No markdown fences. No extra prose.
|
|
38
49
|
|
|
39
50
|
OpenClaw context injection:
|
|
40
|
-
- OpenClaw automatically injects
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
-
|
|
51
|
+
- OpenClaw automatically injects these workspace files into the agent's Project Context: ${renderContextFileList(
|
|
52
|
+
kDoctorRootContextFiles,
|
|
53
|
+
)}.
|
|
54
|
+
- \`BOOTSTRAP.md\` is first-run only; the others above are injected on normal turns when present.
|
|
55
|
+
- Additionally, AlphaClaw injects these extra bootstrap files on normal turns when present: ${renderContextFileList(
|
|
56
|
+
kDoctorBootstrapExtraFiles,
|
|
57
|
+
)}.
|
|
58
|
+
- Large injected files are truncated per-file at ${kDoctorBootstrapMaxChars} chars by default, and total bootstrap injection across files is capped at ${kDoctorBootstrapTotalMaxChars} chars by default.
|
|
59
|
+
- ${kDoctorContextTruncationGuidance}
|
|
44
60
|
|
|
45
61
|
OpenClaw default context:
|
|
46
62
|
- \`AGENTS.md\` is the workspace home file in the default OpenClaw template. It may intentionally include first-run instructions, session-startup guidance, memory conventions, safety rules, tool pointers, and optional behavioral guidance.
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
+
const {
|
|
4
|
+
analyzeBootstrapContext,
|
|
5
|
+
buildBootstrapTruncationCards,
|
|
6
|
+
} = require("./bootstrap-context");
|
|
3
7
|
const { buildDoctorPrompt } = require("./prompt");
|
|
4
8
|
const { normalizeDoctorResult } = require("./normalize");
|
|
5
9
|
const { calculateWorkspaceDelta, computeWorkspaceSnapshot } = require("./workspace-fingerprint");
|
|
@@ -137,6 +141,7 @@ const createDoctorService = ({
|
|
|
137
141
|
};
|
|
138
142
|
|
|
139
143
|
const buildStatus = () => {
|
|
144
|
+
const bootstrapContext = analyzeBootstrapContext({ workspaceRoot });
|
|
140
145
|
const recentRuns = listDoctorRuns({ limit: 10 });
|
|
141
146
|
const latestRun = recentRuns[0] || null;
|
|
142
147
|
const latestCompletedRun =
|
|
@@ -179,6 +184,7 @@ const createDoctorService = ({
|
|
|
179
184
|
lastRunAgeMs,
|
|
180
185
|
needsInitialRun: !latestCompletedRun,
|
|
181
186
|
stale,
|
|
187
|
+
bootstrapContext,
|
|
182
188
|
changeSummary: {
|
|
183
189
|
...delta,
|
|
184
190
|
hasBaseline: hasManifestBaseline,
|
|
@@ -245,10 +251,14 @@ const createDoctorService = ({
|
|
|
245
251
|
console.error(`[doctor] run ${runId} stderr end`);
|
|
246
252
|
throw error;
|
|
247
253
|
}
|
|
248
|
-
|
|
254
|
+
const bootstrapTruncationCards = buildBootstrapTruncationCards(
|
|
255
|
+
analyzeBootstrapContext({ workspaceRoot }),
|
|
256
|
+
);
|
|
257
|
+
const cards = [...bootstrapTruncationCards, ...normalizedResult.cards];
|
|
258
|
+
captureEvidenceSnippets(cards, workspaceRoot);
|
|
249
259
|
insertDoctorCards({
|
|
250
260
|
runId,
|
|
251
|
-
cards
|
|
261
|
+
cards,
|
|
252
262
|
});
|
|
253
263
|
completeDoctorRun({
|
|
254
264
|
id: runId,
|
|
@@ -342,7 +352,11 @@ const createDoctorService = ({
|
|
|
342
352
|
throw new Error("Doctor import requires raw output");
|
|
343
353
|
}
|
|
344
354
|
const normalizedResult = normalizeDoctorResult(normalizedRawOutput);
|
|
345
|
-
|
|
355
|
+
const bootstrapTruncationCards = buildBootstrapTruncationCards(
|
|
356
|
+
analyzeBootstrapContext({ workspaceRoot }),
|
|
357
|
+
);
|
|
358
|
+
const cards = [...bootstrapTruncationCards, ...normalizedResult.cards];
|
|
359
|
+
captureEvidenceSnippets(cards, workspaceRoot);
|
|
346
360
|
const workspaceSnapshot = getCurrentWorkspaceSnapshot();
|
|
347
361
|
const runId = createDoctorRun({
|
|
348
362
|
status: kDoctorRunStatus.completed,
|
|
@@ -354,7 +368,7 @@ const createDoctorService = ({
|
|
|
354
368
|
});
|
|
355
369
|
insertDoctorCards({
|
|
356
370
|
runId,
|
|
357
|
-
cards
|
|
371
|
+
cards,
|
|
358
372
|
});
|
|
359
373
|
completeDoctorRun({
|
|
360
374
|
id: runId,
|
package/lib/server.js
CHANGED
|
@@ -131,7 +131,7 @@ const app = express();
|
|
|
131
131
|
app.set("trust proxy", kTrustProxyHops);
|
|
132
132
|
app.use(["/webhook", "/hooks"], express.raw({ type: "*/*", limit: "5mb" }));
|
|
133
133
|
app.use("/gmail-pubsub", express.raw({ type: "*/*", limit: "5mb" }));
|
|
134
|
-
app.use(express.json());
|
|
134
|
+
app.use(express.json({ limit: "5mb" }));
|
|
135
135
|
|
|
136
136
|
const proxy = httpProxy.createProxyServer({
|
|
137
137
|
target: GATEWAY_URL,
|