@openclaw/plugin-inspector 0.3.3 → 0.3.5
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/CHANGELOG.md +20 -0
- package/package.json +1 -1
- package/src/advanced.js +2 -0
- package/src/ci-summary.js +35 -1
- package/src/cli.js +2 -1
- package/src/cold-import-readiness.js +3 -1
- package/src/compatibility-report.js +3 -0
- package/src/contract-capture.js +1 -0
- package/src/contract-probes.js +14 -0
- package/src/fixture-summary.js +7 -1
- package/src/import-loop-profile.js +36 -4
- package/src/index.js +4 -2
- package/src/inspector.js +15 -1
- package/src/issues.js +1 -1
- package/src/platform-probes.js +56 -11
- package/src/process-profile.js +40 -19
- package/src/report-sanitizer.js +42 -0
- package/src/report.js +10 -5
- package/src/runtime-profile.js +67 -15
- package/src/synthetic-probes.js +6 -0
- package/src/workspace-plan.js +11 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
_No unreleased changes._
|
|
6
|
+
|
|
7
|
+
## 0.3.5 - 2026-04-29
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Add immediate/faster subprocess RSS and CPU sampling plus explicit sample counts so short import-loop reports do not silently publish fake zero-memory metrics.
|
|
12
|
+
- Classify `createChatChannelPlugin` as channel factory metadata in synthetic probe plans so channel-core plugins do not fail as unknown registrars.
|
|
13
|
+
- Treat `createChatChannelPlugin` and `defineChannelPluginEntry` as channel registration equivalents when validating fixture expectations.
|
|
14
|
+
- Label runtime profile wall-time summaries as command-median p95 and render missing sampled metrics as `n/a`.
|
|
15
|
+
|
|
16
|
+
## 0.3.4 - 2026-04-29
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Separate executor-covered platform portability findings from residual findings so downstream structured runners can keep reports blocking only on unhandled risks.
|
|
21
|
+
- Sanitize absolute target OpenClaw paths from generated report artifacts and JSON CLI output.
|
|
22
|
+
- Normalize the dependency-install inspector finding title to use isolated-workspace wording.
|
|
23
|
+
- Treat `openclaw` package dependencies as host-linked workspace inputs instead of isolated dependency-install blockers.
|
|
24
|
+
|
|
5
25
|
## 0.3.3 - 2026-04-28
|
|
6
26
|
|
|
7
27
|
### Fixed
|
package/package.json
CHANGED
package/src/advanced.js
CHANGED
|
@@ -43,6 +43,7 @@ export {
|
|
|
43
43
|
} from "./ci-outputs.js";
|
|
44
44
|
export {
|
|
45
45
|
buildContractProbes,
|
|
46
|
+
compatRecordForIssueCode,
|
|
46
47
|
contractProbeRules,
|
|
47
48
|
probePriority,
|
|
48
49
|
} from "./contract-probes.js";
|
|
@@ -170,6 +171,7 @@ export {
|
|
|
170
171
|
classifyCompatRecordCoverage,
|
|
171
172
|
renderMarkdownReport,
|
|
172
173
|
renderTextSummary,
|
|
174
|
+
sanitizeReportArtifact,
|
|
173
175
|
writeCompatibilityReport,
|
|
174
176
|
writeReport,
|
|
175
177
|
} from "./report.js";
|
package/src/ci-summary.js
CHANGED
|
@@ -58,6 +58,8 @@ export async function buildCiSummary(options = {}) {
|
|
|
58
58
|
importLoopP95Ms: reports.importLoop?.summary?.p95WallMs ?? 0,
|
|
59
59
|
importLoopMaxRssMb: reports.importLoop?.summary?.maxPeakRssMb ?? 0,
|
|
60
60
|
importLoopMaxCpuMs: reports.importLoop?.summary?.maxCpuMsEstimate ?? 0,
|
|
61
|
+
importLoopRssSampleCount: metricSampleCount(reports.importLoop, "rss", "maxPeakRssMb"),
|
|
62
|
+
importLoopCpuSampleCount: metricSampleCount(reports.importLoop, "cpu", "maxCpuMsEstimate"),
|
|
61
63
|
},
|
|
62
64
|
topIssues: topIssues(reports.compatibility),
|
|
63
65
|
refRegressions: (reports.refDiff?.regressions ?? []).slice(0, 20),
|
|
@@ -148,7 +150,7 @@ export function renderCiSummaryMarkdown(summary) {
|
|
|
148
150
|
["Jiti loader candidates", summary.summary.loaderJitiCandidates],
|
|
149
151
|
[
|
|
150
152
|
"Import loop",
|
|
151
|
-
`p50 ${summary.summary.importLoopP50Ms} ms / p95 ${summary.summary.importLoopP95Ms} ms / max RSS ${summary.summary.importLoopMaxRssMb}
|
|
153
|
+
`p50 ${summary.summary.importLoopP50Ms} ms / p95 ${summary.summary.importLoopP95Ms} ms / max RSS ${formatSampledMetric(summary.summary.importLoopMaxRssMb, summary.summary.importLoopRssSampleCount)} / CPU ${formatSampledMetric(summary.summary.importLoopMaxCpuMs, summary.summary.importLoopCpuSampleCount, "ms")}`,
|
|
152
154
|
],
|
|
153
155
|
],
|
|
154
156
|
["Metric", "Value"],
|
|
@@ -221,3 +223,35 @@ function topIssues(report) {
|
|
|
221
223
|
function markdownTable(rows, headers) {
|
|
222
224
|
return renderPaddedMarkdownTable(rows, headers, { nullValue: "-" });
|
|
223
225
|
}
|
|
226
|
+
|
|
227
|
+
function metricSampleCount(report, kind, maxMetric) {
|
|
228
|
+
const summaryKey = kind === "rss" ? "rssSampleCount" : "cpuSampleCount";
|
|
229
|
+
const summaryCount = report?.summary?.[summaryKey];
|
|
230
|
+
if (Number.isFinite(summaryCount)) {
|
|
231
|
+
return summaryCount;
|
|
232
|
+
}
|
|
233
|
+
const sampleCount = inferSampleCount(report?.samples, kind);
|
|
234
|
+
if (sampleCount > 0) {
|
|
235
|
+
return sampleCount;
|
|
236
|
+
}
|
|
237
|
+
return (report?.summary?.[maxMetric] ?? 0) > 0 ? 1 : 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function inferSampleCount(samples = [], kind) {
|
|
241
|
+
if (!Array.isArray(samples)) {
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
return samples.reduce((sum, sample) => {
|
|
245
|
+
if (kind === "rss") {
|
|
246
|
+
return sum + (sample.rssSampleCount ?? (sample.peakRssMb > 0 ? 1 : 0));
|
|
247
|
+
}
|
|
248
|
+
return sum + (sample.cpuSampleCount ?? 0);
|
|
249
|
+
}, 0);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function formatSampledMetric(value, count, unit = "MB") {
|
|
253
|
+
if ((count ?? 0) <= 0) {
|
|
254
|
+
return "n/a";
|
|
255
|
+
}
|
|
256
|
+
return `${value} ${unit}`;
|
|
257
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import {
|
|
4
4
|
loadPluginConfig,
|
|
5
5
|
renderTextSummary,
|
|
6
|
+
sanitizeReportArtifact,
|
|
6
7
|
runPluginCheck,
|
|
7
8
|
} from "./index.js";
|
|
8
9
|
import {
|
|
@@ -90,7 +91,7 @@ async function runCheck(commandArgs) {
|
|
|
90
91
|
});
|
|
91
92
|
|
|
92
93
|
if (json) {
|
|
93
|
-
console.log(JSON.stringify(report, null, 2));
|
|
94
|
+
console.log(JSON.stringify(sanitizeReportArtifact(report), null, 2));
|
|
94
95
|
} else {
|
|
95
96
|
console.log(renderTextSummary(report, { artifacts: paths }));
|
|
96
97
|
}
|
|
@@ -3,6 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
import { renderPaddedMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
|
|
4
4
|
import { slugForArtifact } from "./path-utils.js";
|
|
5
5
|
|
|
6
|
+
const hostLinkedRuntimeDependencies = new Set(["openclaw"]);
|
|
7
|
+
|
|
6
8
|
export function buildColdImportReadiness(options = {}) {
|
|
7
9
|
const report = options.report;
|
|
8
10
|
if (!report) {
|
|
@@ -182,7 +184,7 @@ function classifyEntrypointReadiness({ fixture, packageSummary, entrypoint, root
|
|
|
182
184
|
...(packageSummary.dependencies ?? []),
|
|
183
185
|
...(packageSummary.peerDependencies ?? []),
|
|
184
186
|
...(packageSummary.optionalDependencies ?? []),
|
|
185
|
-
]);
|
|
187
|
+
]).filter((dependency) => !hostLinkedRuntimeDependencies.has(dependency));
|
|
186
188
|
if (entrypoint.exists && runtimeDependencies.length > 0) {
|
|
187
189
|
blockers.push({
|
|
188
190
|
code: "dependency-install-required",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { renderPaddedMarkdownTable } from "./artifacts.js";
|
|
2
|
+
import { sanitizeReportArtifact } from "./report-sanitizer.js";
|
|
2
3
|
|
|
3
4
|
const defaultSeverityLabels = {
|
|
4
5
|
P0: "P0",
|
|
@@ -8,6 +9,7 @@ const defaultSeverityLabels = {
|
|
|
8
9
|
};
|
|
9
10
|
|
|
10
11
|
export function renderCompatibilityMarkdownReport(report, options = {}) {
|
|
12
|
+
report = sanitizeReportArtifact(report, options);
|
|
11
13
|
return [
|
|
12
14
|
`# ${options.title ?? "OpenClaw Plugin Compatibility Report"}`,
|
|
13
15
|
"",
|
|
@@ -127,6 +129,7 @@ export function renderCompatibilityMarkdownReport(report, options = {}) {
|
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
export function renderCompatibilityIssuesReport(report, options = {}) {
|
|
132
|
+
report = sanitizeReportArtifact(report, options);
|
|
130
133
|
return [
|
|
131
134
|
`# ${options.title ?? "OpenClaw Plugin Issue Findings"}`,
|
|
132
135
|
"",
|
package/src/contract-capture.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "./synthetic-probes.js";
|
|
8
8
|
|
|
9
9
|
export const defaultRegistrationAssertions = {
|
|
10
|
+
createChatChannelPlugin: ["channel plugin id is stable", "channel factory metadata is captured"],
|
|
10
11
|
defineChannelPluginEntry: ["channel id is stable", "setup/config schema can be read", "message envelope metadata is preserved"],
|
|
11
12
|
definePluginEntry: ["entrypoint register function is callable", "entrypoint metadata is preserved"],
|
|
12
13
|
registerChannel: ["channel id is stable", "inbound/outbound envelope shape is captured", "sender metadata is preserved"],
|
package/src/contract-probes.js
CHANGED
|
@@ -106,6 +106,20 @@ export const contractProbeRules = {
|
|
|
106
106
|
},
|
|
107
107
|
};
|
|
108
108
|
|
|
109
|
+
const openClawOwnedProbeIssueCodes = new Set([
|
|
110
|
+
"before-tool-call-probe",
|
|
111
|
+
"channel-contract-probe",
|
|
112
|
+
"conversation-access-hook",
|
|
113
|
+
"registration-capture-gap",
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
export function compatRecordForIssueCode(code) {
|
|
117
|
+
if (!openClawOwnedProbeIssueCodes.has(code)) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
return contractProbeRules[code]?.id;
|
|
121
|
+
}
|
|
122
|
+
|
|
109
123
|
export function buildContractProbes({ warnings = [], suggestions = [], fixtures = [] }) {
|
|
110
124
|
const fixtureById = new Map(fixtures.map((fixture) => [fixture.id, fixture]));
|
|
111
125
|
const probes = [];
|
package/src/fixture-summary.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { readdir } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { compatRecordForIssueCode } from "./contract-probes.js";
|
|
4
5
|
import { readJsonFile } from "./json-file.js";
|
|
5
6
|
|
|
6
7
|
const conversationAccessHooks = new Set(["agent_end", "llm_input", "llm_output"]);
|
|
@@ -17,6 +18,7 @@ const channelRegistrations = new Set([
|
|
|
17
18
|
"defineChannelPluginEntry",
|
|
18
19
|
"registerChannel",
|
|
19
20
|
]);
|
|
21
|
+
const hostLinkedRuntimeDependencies = new Set(["openclaw"]);
|
|
20
22
|
|
|
21
23
|
export async function buildCompatibilityFixtureReport({ fixture, inspection, checkoutPath, sourceRoot, rootDir = process.cwd() }) {
|
|
22
24
|
const pluginManifests = await readPluginManifests({ checkoutPath, sourceRoot, rootDir });
|
|
@@ -278,7 +280,7 @@ export function classifyPackageContracts({ fixture, inspection, fixtureReport })
|
|
|
278
280
|
...packageSummary.dependencies,
|
|
279
281
|
...packageSummary.peerDependencies,
|
|
280
282
|
...packageSummary.optionalDependencies,
|
|
281
|
-
]);
|
|
283
|
+
]).filter((dependency) => !hostLinkedRuntimeDependencies.has(dependency));
|
|
282
284
|
if (packageSummary.openclaw?.entrypoints.length > 0 && runtimeDependencies.length > 0) {
|
|
283
285
|
suggestions.push({
|
|
284
286
|
fixture: fixture.id,
|
|
@@ -399,6 +401,7 @@ export function classifyCompatibilityFixture({ fixture, inspection, fixtureRepor
|
|
|
399
401
|
level: "warning",
|
|
400
402
|
message: "fixture observes raw model or conversation content and needs privacy-boundary contract probes",
|
|
401
403
|
evidence: detailEvidence(conversationHookDetails),
|
|
404
|
+
compatRecord: compatRecordForIssueCode("conversation-access-hook"),
|
|
402
405
|
});
|
|
403
406
|
decisions.push({
|
|
404
407
|
fixture: fixture.id,
|
|
@@ -459,6 +462,7 @@ export function classifyCompatibilityFixture({ fixture, inspection, fixtureRepor
|
|
|
459
462
|
level: "suggestion",
|
|
460
463
|
message: "future inspector capture API should record lifecycle, route, gateway, command, and interactive registrations",
|
|
461
464
|
evidence: detailEvidence(captureGapRegistrationDetails),
|
|
465
|
+
compatRecord: compatRecordForIssueCode("registration-capture-gap"),
|
|
462
466
|
});
|
|
463
467
|
decisions.push({
|
|
464
468
|
fixture: fixture.id,
|
|
@@ -477,6 +481,7 @@ export function classifyCompatibilityFixture({ fixture, inspection, fixtureRepor
|
|
|
477
481
|
level: "suggestion",
|
|
478
482
|
message: "add contract probes for before_tool_call terminal, block, and approval semantics",
|
|
479
483
|
evidence: detailEvidence(hookDetails),
|
|
484
|
+
compatRecord: compatRecordForIssueCode("before-tool-call-probe"),
|
|
480
485
|
});
|
|
481
486
|
decisions.push({
|
|
482
487
|
fixture: fixture.id,
|
|
@@ -500,6 +505,7 @@ export function classifyCompatibilityFixture({ fixture, inspection, fixtureRepor
|
|
|
500
505
|
level: "suggestion",
|
|
501
506
|
message: "add channel envelope, config-schema, and runtime metadata probes",
|
|
502
507
|
evidence: detailEvidence(channelRegistrationDetails),
|
|
508
|
+
compatRecord: compatRecordForIssueCode("channel-contract-probe"),
|
|
503
509
|
});
|
|
504
510
|
decisions.push({
|
|
505
511
|
fixture: fixture.id,
|
|
@@ -30,6 +30,9 @@ export async function buildImportLoopProfile(options = {}) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
const wallMs = samples.map((sample) => sample.wallMs).sort((left, right) => left - right);
|
|
33
|
+
const rssSampleCount = samples.reduce((sum, sample) => sum + (sample.rssSampleCount ?? (sample.peakRssMb > 0 ? 1 : 0)), 0);
|
|
34
|
+
const cpuSampleCount = samples.reduce((sum, sample) => sum + (sample.cpuSampleCount ?? 0), 0);
|
|
35
|
+
const statSampleCount = samples.reduce((sum, sample) => sum + (sample.statSampleCount ?? 0), 0);
|
|
33
36
|
return {
|
|
34
37
|
generatedAt: options.generatedAt ?? defaultImportLoopProfileOptions.generatedAt,
|
|
35
38
|
mode: options.mode ?? "subprocess-cold-import-loop",
|
|
@@ -40,6 +43,9 @@ export async function buildImportLoopProfile(options = {}) {
|
|
|
40
43
|
p95WallMs: percentile(wallMs, 0.95),
|
|
41
44
|
maxPeakRssMb: Math.max(0, ...samples.map((sample) => sample.peakRssMb)),
|
|
42
45
|
maxCpuMsEstimate: Math.max(0, ...samples.map((sample) => sample.cpuMsEstimate)),
|
|
46
|
+
statSampleCount,
|
|
47
|
+
rssSampleCount,
|
|
48
|
+
cpuSampleCount,
|
|
43
49
|
capturedCount: samples.reduce((sum, sample) => sum + sample.capturedCount, 0),
|
|
44
50
|
failCount: samples.filter((sample) => sample.exitCode !== 0 || sample.status !== "captured").length,
|
|
45
51
|
},
|
|
@@ -85,7 +91,7 @@ export function renderImportLoopProfileMarkdown(report, options = {}) {
|
|
|
85
91
|
"",
|
|
86
92
|
"## Summary",
|
|
87
93
|
"",
|
|
88
|
-
markdownTable(
|
|
94
|
+
markdownTable(summaryRows(report), ["Metric", "Value"]),
|
|
89
95
|
"",
|
|
90
96
|
"## Samples",
|
|
91
97
|
"",
|
|
@@ -95,11 +101,12 @@ export function renderImportLoopProfileMarkdown(report, options = {}) {
|
|
|
95
101
|
sample.status,
|
|
96
102
|
sample.capturedCount,
|
|
97
103
|
`${sample.wallMs} ms`,
|
|
98
|
-
|
|
99
|
-
|
|
104
|
+
formatSampledMetric(sample.peakRssMb, sample.rssSampleCount),
|
|
105
|
+
formatSampledMetric(sample.cpuMsEstimate, sample.cpuSampleCount, "ms"),
|
|
106
|
+
`${sample.rssSampleCount ?? 0}/${sample.cpuSampleCount ?? 0}`,
|
|
100
107
|
sample.exitCode,
|
|
101
108
|
]),
|
|
102
|
-
["Run", "Status", "Captured", "Wall", "Peak RSS", "CPU Estimate", "Exit"],
|
|
109
|
+
["Run", "Status", "Captured", "Wall", "Peak RSS", "CPU Estimate", "RSS/CPU samples", "Exit"],
|
|
103
110
|
),
|
|
104
111
|
].join("\n");
|
|
105
112
|
}
|
|
@@ -130,10 +137,35 @@ async function runCaptureSample(options) {
|
|
|
130
137
|
peakRssMb: profile.peakRssMb,
|
|
131
138
|
peakCpuPercent: profile.peakCpuPercent,
|
|
132
139
|
cpuMsEstimate: profile.cpuMsEstimate,
|
|
140
|
+
statSampleCount: profile.statSampleCount,
|
|
141
|
+
rssSampleCount: profile.rssSampleCount,
|
|
142
|
+
cpuSampleCount: profile.cpuSampleCount,
|
|
133
143
|
stderrPreview: profile.stderrPreview,
|
|
134
144
|
};
|
|
135
145
|
}
|
|
136
146
|
|
|
147
|
+
function summaryRows(report) {
|
|
148
|
+
return [
|
|
149
|
+
["runs", report.summary.runs],
|
|
150
|
+
["p50WallMs", report.summary.p50WallMs],
|
|
151
|
+
["p95WallMs", report.summary.p95WallMs],
|
|
152
|
+
["maxPeakRssMb", formatSampledMetric(report.summary.maxPeakRssMb, report.summary.rssSampleCount)],
|
|
153
|
+
["maxCpuMsEstimate", formatSampledMetric(report.summary.maxCpuMsEstimate, report.summary.cpuSampleCount, "ms")],
|
|
154
|
+
["statSampleCount", report.summary.statSampleCount ?? 0],
|
|
155
|
+
["rssSampleCount", report.summary.rssSampleCount ?? 0],
|
|
156
|
+
["cpuSampleCount", report.summary.cpuSampleCount ?? 0],
|
|
157
|
+
["capturedCount", report.summary.capturedCount],
|
|
158
|
+
["failCount", report.summary.failCount],
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function formatSampledMetric(value, count, unit = "MB") {
|
|
163
|
+
if ((count ?? 0) <= 0) {
|
|
164
|
+
return "n/a";
|
|
165
|
+
}
|
|
166
|
+
return `${value} ${unit}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
137
169
|
function buildCaptureCommand(options) {
|
|
138
170
|
if (typeof options.captureCommand === "function") {
|
|
139
171
|
return options.captureCommand({
|
package/src/index.js
CHANGED
|
@@ -53,11 +53,13 @@ export const staticInspection = Object.freeze({
|
|
|
53
53
|
export const reports = Object.freeze({
|
|
54
54
|
renderMarkdown: reportApi.renderMarkdownReport,
|
|
55
55
|
renderTextSummary: pluginApi.renderTextSummary,
|
|
56
|
+
sanitizeArtifact: reportApi.sanitizeReportArtifact,
|
|
56
57
|
write: reportApi.writeReport,
|
|
57
58
|
issueId: issuesApi.issueId,
|
|
58
59
|
classifyIssueFinding: issuesApi.classifyIssueFinding,
|
|
59
60
|
knownIssueCodes: issuesApi.knownIssueCodes,
|
|
60
61
|
openClawTargetPathCandidates: openClawTargetApi.openClawTargetPathCandidates,
|
|
62
|
+
readOpenClawTargetSurface: openClawTargetApi.readOpenClawTargetSurface,
|
|
61
63
|
});
|
|
62
64
|
|
|
63
65
|
export const contracts = Object.freeze({
|
|
@@ -200,7 +202,7 @@ export {
|
|
|
200
202
|
} from "./import-loop-profile.js";
|
|
201
203
|
export { classifyIssueFinding, issueId, knownIssueCodes } from "./issues.js";
|
|
202
204
|
export { inspectFixtureSet, inspectPlugin, inspectSourceText } from "./inspector.js";
|
|
203
|
-
export { openClawTargetPathCandidates } from "./openclaw-target.js";
|
|
205
|
+
export { openClawTargetPathCandidates, readOpenClawTargetSurface } from "./openclaw-target.js";
|
|
204
206
|
export {
|
|
205
207
|
buildProfileDiff,
|
|
206
208
|
defaultProfileDiffOptions,
|
|
@@ -216,7 +218,7 @@ export {
|
|
|
216
218
|
validateRefDiff,
|
|
217
219
|
writeRefDiff,
|
|
218
220
|
} from "./ref-diff.js";
|
|
219
|
-
export { renderMarkdownReport, writeReport } from "./report.js";
|
|
221
|
+
export { renderMarkdownReport, sanitizeReportArtifact, writeReport } from "./report.js";
|
|
220
222
|
export {
|
|
221
223
|
buildRuntimeProfile,
|
|
222
224
|
defaultRuntimeProfileCommands,
|
package/src/inspector.js
CHANGED
|
@@ -11,6 +11,9 @@ import { readOpenClawTargetSurface } from "./openclaw-target.js";
|
|
|
11
11
|
import { buildCompatibilityReport, buildReport } from "./report.js";
|
|
12
12
|
|
|
13
13
|
const execFileAsync = promisify(execFile);
|
|
14
|
+
const registrationEquivalents = new Map([
|
|
15
|
+
["registerChannel", new Set(["createChatChannelPlugin", "defineChannelPluginEntry", "registerChannel"])],
|
|
16
|
+
]);
|
|
14
17
|
|
|
15
18
|
export async function inspectFixtureSet(config, options = {}) {
|
|
16
19
|
const { inspections, failures } = await inspectConfiguredFixtures(config, options);
|
|
@@ -58,7 +61,7 @@ async function inspectConfiguredFixtures(config, options = {}) {
|
|
|
58
61
|
["manifestContracts", inspection.manifestContracts],
|
|
59
62
|
]) {
|
|
60
63
|
const expected = fixture.expect?.[key] ?? [];
|
|
61
|
-
const missing = expected.filter((value) => !
|
|
64
|
+
const missing = expected.filter((value) => !satisfiesExpectedSeam(key, value, observed));
|
|
62
65
|
if (missing.length > 0) {
|
|
63
66
|
failures.push(`${fixture.id}: missing ${key}: ${missing.join(", ")}`);
|
|
64
67
|
}
|
|
@@ -68,6 +71,17 @@ async function inspectConfiguredFixtures(config, options = {}) {
|
|
|
68
71
|
return { inspections, failures };
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
function satisfiesExpectedSeam(key, expected, observed) {
|
|
75
|
+
if (observed.includes(expected)) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
if (key !== "registrations") {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const equivalents = registrationEquivalents.get(expected);
|
|
82
|
+
return Boolean(equivalents && observed.some((value) => equivalents.has(value)));
|
|
83
|
+
}
|
|
84
|
+
|
|
71
85
|
export async function inspectPlugin(fixture, options = {}) {
|
|
72
86
|
const config = options.config ?? { rootDir: options.rootDir ?? process.cwd() };
|
|
73
87
|
const checkoutPath = fixtureCheckoutPath(config, fixture);
|
package/src/issues.js
CHANGED
|
@@ -119,7 +119,7 @@ export const issueMetadataByCode = {
|
|
|
119
119
|
severity: "P2",
|
|
120
120
|
owner: "inspector",
|
|
121
121
|
decision: "inspector-follow-up",
|
|
122
|
-
title: "cold import requires
|
|
122
|
+
title: "cold import requires dependency installation in an isolated workspace",
|
|
123
123
|
},
|
|
124
124
|
"package-entrypoint-missing": {
|
|
125
125
|
severity: "P1",
|
package/src/platform-probes.js
CHANGED
|
@@ -12,13 +12,11 @@ export function buildPlatformProbes(options = {}) {
|
|
|
12
12
|
const entrypoints = plan.fixtures.flatMap((fixture) =>
|
|
13
13
|
fixture.entrypoints.map((entrypoint) => summarizeEntrypoint(fixture.id, entrypoint)),
|
|
14
14
|
);
|
|
15
|
-
const
|
|
16
|
-
fixture.entrypoints.flatMap((entrypoint) =>
|
|
17
|
-
entrypoint.steps
|
|
18
|
-
.map((step) => summarizeStep(fixture.id, entrypoint, step))
|
|
19
|
-
.filter((finding) => finding.riskCodes.length > 0),
|
|
20
|
-
),
|
|
15
|
+
const stepFindings = plan.fixtures.flatMap((fixture) =>
|
|
16
|
+
fixture.entrypoints.flatMap((entrypoint) => entrypoint.steps.map((step) => summarizeStep(fixture.id, entrypoint, step, options.stepCoverage))),
|
|
21
17
|
);
|
|
18
|
+
const portabilityFindings = stepFindings.flatMap((finding) => (finding.residual ? [finding.residual] : []));
|
|
19
|
+
const coveredPortabilityFindings = stepFindings.flatMap((finding) => (finding.covered ? [finding.covered] : []));
|
|
22
20
|
|
|
23
21
|
return {
|
|
24
22
|
generatedAt: plan.generatedAt,
|
|
@@ -31,6 +29,7 @@ export function buildPlatformProbes(options = {}) {
|
|
|
31
29
|
jitiAlternativeCount: entrypoints.filter((entrypoint) => entrypoint.loaderAlternatives.includes("jiti")).length,
|
|
32
30
|
lazyImportProbeCount: entrypoints.filter((entrypoint) => entrypoint.capturePlanned && entrypoint.syntheticProbePlanned).length,
|
|
33
31
|
portabilityFindingCount: portabilityFindings.length,
|
|
32
|
+
coveredPortabilityFindingCount: coveredPortabilityFindings.length,
|
|
34
33
|
windowsRiskStepCount: portabilityFindings.filter((finding) => finding.platforms.includes("windows")).length,
|
|
35
34
|
macosRiskStepCount: portabilityFindings.filter((finding) => finding.platforms.includes("macos")).length,
|
|
36
35
|
linuxRiskStepCount: portabilityFindings.filter((finding) => finding.platforms.includes("linux")).length,
|
|
@@ -38,6 +37,7 @@ export function buildPlatformProbes(options = {}) {
|
|
|
38
37
|
},
|
|
39
38
|
entrypoints,
|
|
40
39
|
portabilityFindings,
|
|
40
|
+
coveredPortabilityFindings,
|
|
41
41
|
recommendations: buildRecommendations(portabilityFindings, entrypoints),
|
|
42
42
|
};
|
|
43
43
|
}
|
|
@@ -112,6 +112,19 @@ export function renderPlatformProbesMarkdown(report, options = {}) {
|
|
|
112
112
|
["Fixture", "Step", "Platforms", "Risks", "Mitigation"],
|
|
113
113
|
),
|
|
114
114
|
"",
|
|
115
|
+
"## Covered Portability Findings",
|
|
116
|
+
"",
|
|
117
|
+
markdownTable(
|
|
118
|
+
(report.coveredPortabilityFindings ?? []).map((finding) => [
|
|
119
|
+
finding.fixture,
|
|
120
|
+
finding.kind,
|
|
121
|
+
finding.platforms.join(", ") || "-",
|
|
122
|
+
finding.riskCodes.join(", "),
|
|
123
|
+
finding.coverage,
|
|
124
|
+
]),
|
|
125
|
+
["Fixture", "Step", "Platforms", "Covered Risks", "Coverage"],
|
|
126
|
+
),
|
|
127
|
+
"",
|
|
115
128
|
"## Recommendations",
|
|
116
129
|
"",
|
|
117
130
|
markdownTable(
|
|
@@ -140,16 +153,48 @@ function summarizeEntrypoint(fixtureId, entrypoint) {
|
|
|
140
153
|
};
|
|
141
154
|
}
|
|
142
155
|
|
|
143
|
-
function summarizeStep(fixtureId, entrypoint, step) {
|
|
156
|
+
function summarizeStep(fixtureId, entrypoint, step, stepCoverage) {
|
|
144
157
|
const riskCodes = stepRiskCodes(step);
|
|
145
|
-
|
|
158
|
+
const coverage = normalizeStepCoverage(stepCoverage?.({ fixture: fixtureId, entrypoint, step, riskCodes }), riskCodes);
|
|
159
|
+
const residualRiskCodes = riskCodes.filter((code) => !coverage.riskCodes.includes(code));
|
|
160
|
+
const common = {
|
|
146
161
|
fixture: fixtureId,
|
|
147
162
|
entrypoint: entrypoint.id,
|
|
148
163
|
kind: step.kind,
|
|
149
|
-
platforms: platformsForRiskCodes(riskCodes),
|
|
150
|
-
riskCodes,
|
|
151
164
|
command: step.command,
|
|
152
|
-
|
|
165
|
+
};
|
|
166
|
+
return {
|
|
167
|
+
residual:
|
|
168
|
+
residualRiskCodes.length > 0
|
|
169
|
+
? {
|
|
170
|
+
...common,
|
|
171
|
+
coveredRiskCodes: coverage.riskCodes,
|
|
172
|
+
platforms: platformsForRiskCodes(residualRiskCodes),
|
|
173
|
+
riskCodes: residualRiskCodes,
|
|
174
|
+
mitigation: mitigationForRiskCodes(residualRiskCodes),
|
|
175
|
+
}
|
|
176
|
+
: null,
|
|
177
|
+
covered:
|
|
178
|
+
coverage.riskCodes.length > 0
|
|
179
|
+
? {
|
|
180
|
+
...common,
|
|
181
|
+
coverage: coverage.reason,
|
|
182
|
+
platforms: platformsForRiskCodes(coverage.riskCodes),
|
|
183
|
+
riskCodes: coverage.riskCodes,
|
|
184
|
+
}
|
|
185
|
+
: null,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function normalizeStepCoverage(coverage, riskCodes) {
|
|
190
|
+
if (!coverage) {
|
|
191
|
+
return { reason: "", riskCodes: [] };
|
|
192
|
+
}
|
|
193
|
+
const requested = coverage === true ? riskCodes : coverage.coveredRiskCodes ?? coverage.handledRiskCodes ?? coverage.riskCodes ?? coverage;
|
|
194
|
+
const covered = Array.isArray(requested) ? requested : [];
|
|
195
|
+
return {
|
|
196
|
+
reason: typeof coverage.reason === "string" && coverage.reason ? coverage.reason : "covered by isolated workspace executor",
|
|
197
|
+
riskCodes: covered.filter((code) => riskCodes.includes(code)).sort(),
|
|
153
198
|
};
|
|
154
199
|
}
|
|
155
200
|
|
package/src/process-profile.js
CHANGED
|
@@ -7,8 +7,12 @@ export async function runProfiledProcess(options) {
|
|
|
7
7
|
let firstRssKb = 0;
|
|
8
8
|
let peakRssKb = 0;
|
|
9
9
|
let peakCpuPercent = 0;
|
|
10
|
+
let statSampleCount = 0;
|
|
11
|
+
let rssSampleCount = 0;
|
|
12
|
+
let cpuSampleCount = 0;
|
|
10
13
|
const cpuSamples = [];
|
|
11
14
|
let pollInFlight = false;
|
|
15
|
+
const pendingStats = new Set();
|
|
12
16
|
|
|
13
17
|
const child = spawn(options.command, options.args ?? [], {
|
|
14
18
|
cwd: options.cwd,
|
|
@@ -21,27 +25,43 @@ export async function runProfiledProcess(options) {
|
|
|
21
25
|
child.stderr?.on("data", (chunk) => stderr.push(chunk));
|
|
22
26
|
|
|
23
27
|
const recordStats = (stats) => {
|
|
24
|
-
if (stats.
|
|
28
|
+
if (stats.rssAvailable || stats.cpuAvailable) {
|
|
29
|
+
statSampleCount += 1;
|
|
30
|
+
}
|
|
31
|
+
if (stats.rssAvailable) {
|
|
32
|
+
rssSampleCount += 1;
|
|
33
|
+
}
|
|
34
|
+
if (stats.cpuAvailable) {
|
|
35
|
+
cpuSampleCount += 1;
|
|
36
|
+
}
|
|
37
|
+
if (stats.rssAvailable && stats.rssKb > 0 && firstRssKb === 0) {
|
|
25
38
|
firstRssKb = stats.rssKb;
|
|
26
39
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
40
|
+
if (stats.rssAvailable) {
|
|
41
|
+
peakRssKb = Math.max(peakRssKb, stats.rssKb);
|
|
42
|
+
}
|
|
43
|
+
if (stats.cpuAvailable) {
|
|
44
|
+
peakCpuPercent = Math.max(peakCpuPercent, stats.cpuPercent);
|
|
30
45
|
cpuSamples.push(stats.cpuPercent);
|
|
31
46
|
}
|
|
32
47
|
};
|
|
33
48
|
|
|
34
|
-
const
|
|
49
|
+
const sampleStats = () => {
|
|
35
50
|
if (pollInFlight) {
|
|
36
51
|
return;
|
|
37
52
|
}
|
|
38
53
|
pollInFlight = true;
|
|
39
|
-
readProcessStats(child.pid)
|
|
54
|
+
const pending = readProcessStats(child.pid)
|
|
40
55
|
.then(recordStats)
|
|
41
56
|
.finally(() => {
|
|
42
57
|
pollInFlight = false;
|
|
58
|
+
pendingStats.delete(pending);
|
|
43
59
|
});
|
|
44
|
-
|
|
60
|
+
pendingStats.add(pending);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
sampleStats();
|
|
64
|
+
const poll = setInterval(sampleStats, options.pollMs ?? 25);
|
|
45
65
|
|
|
46
66
|
const exitCode = await new Promise((resolve, reject) => {
|
|
47
67
|
child.on("error", (error) => {
|
|
@@ -51,16 +71,10 @@ export async function runProfiledProcess(options) {
|
|
|
51
71
|
child.on("exit", (code) => resolve(code ?? 1));
|
|
52
72
|
});
|
|
53
73
|
clearInterval(poll);
|
|
74
|
+
await Promise.allSettled([...pendingStats]);
|
|
54
75
|
|
|
55
76
|
const finalStats = await readProcessStats(child.pid);
|
|
56
|
-
|
|
57
|
-
firstRssKb = finalStats.rssKb;
|
|
58
|
-
}
|
|
59
|
-
peakRssKb = Math.max(peakRssKb, finalStats.rssKb);
|
|
60
|
-
peakCpuPercent = Math.max(peakCpuPercent, finalStats.cpuPercent);
|
|
61
|
-
if (finalStats.cpuPercent > 0) {
|
|
62
|
-
cpuSamples.push(finalStats.cpuPercent);
|
|
63
|
-
}
|
|
77
|
+
recordStats(finalStats);
|
|
64
78
|
|
|
65
79
|
const wallMs = Math.round(performance.now() - start);
|
|
66
80
|
const averageCpuPercent =
|
|
@@ -79,6 +93,9 @@ export async function runProfiledProcess(options) {
|
|
|
79
93
|
peakCpuPercent: Math.round(peakCpuPercent * 10) / 10,
|
|
80
94
|
cpuMsEstimate: Math.round((wallMs * cpuPercentForEstimate) / 100),
|
|
81
95
|
harnessHeapDeltaMb: Math.round((heapUsedMb() - heapStartMb) * 10) / 10,
|
|
96
|
+
statSampleCount,
|
|
97
|
+
rssSampleCount,
|
|
98
|
+
cpuSampleCount,
|
|
82
99
|
exitCode,
|
|
83
100
|
stdoutPreview: previewLines(stdout),
|
|
84
101
|
stderrPreview: previewLines(stderr),
|
|
@@ -87,7 +104,7 @@ export async function runProfiledProcess(options) {
|
|
|
87
104
|
|
|
88
105
|
async function readProcessStats(pid) {
|
|
89
106
|
if (!pid || process.platform === "win32") {
|
|
90
|
-
return { rssKb: 0, cpuPercent: 0 };
|
|
107
|
+
return { rssAvailable: false, rssKb: 0, cpuAvailable: false, cpuPercent: 0 };
|
|
91
108
|
}
|
|
92
109
|
return new Promise((resolve) => {
|
|
93
110
|
const ps = spawn("ps", ["-o", "rss=", "-o", "%cpu=", "-p", String(pid)], {
|
|
@@ -95,14 +112,18 @@ async function readProcessStats(pid) {
|
|
|
95
112
|
});
|
|
96
113
|
const chunks = [];
|
|
97
114
|
ps.stdout.on("data", (chunk) => chunks.push(chunk));
|
|
98
|
-
ps.on("error", () => resolve({ rssKb: 0, cpuPercent: 0 }));
|
|
115
|
+
ps.on("error", () => resolve({ rssAvailable: false, rssKb: 0, cpuAvailable: false, cpuPercent: 0 }));
|
|
99
116
|
ps.on("exit", () => {
|
|
100
117
|
const [rssRaw, cpuRaw] = Buffer.concat(chunks).toString("utf8").trim().split(/\s+/);
|
|
101
118
|
const rssKb = Number.parseInt(rssRaw, 10);
|
|
102
119
|
const cpuPercent = Number.parseFloat(cpuRaw);
|
|
120
|
+
const rssAvailable = Number.isFinite(rssKb);
|
|
121
|
+
const cpuAvailable = Number.isFinite(cpuPercent);
|
|
103
122
|
resolve({
|
|
104
|
-
|
|
105
|
-
|
|
123
|
+
rssAvailable,
|
|
124
|
+
rssKb: rssAvailable ? rssKb : 0,
|
|
125
|
+
cpuAvailable,
|
|
126
|
+
cpuPercent: cpuAvailable ? cpuPercent : 0,
|
|
106
127
|
});
|
|
107
128
|
});
|
|
108
129
|
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export function sanitizeReportArtifact(report, options = {}) {
|
|
4
|
+
const sensitivePaths = sensitiveOpenClawPaths(report);
|
|
5
|
+
if (sensitivePaths.length === 0) {
|
|
6
|
+
return report;
|
|
7
|
+
}
|
|
8
|
+
const placeholder = options.openclawPathPlaceholder ?? "<OPENCLAW_PATH>";
|
|
9
|
+
return sanitizeValue(report, sensitivePaths, placeholder);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function sensitiveOpenClawPaths(report) {
|
|
13
|
+
const targetOpenClaw = report?.targetOpenClaw;
|
|
14
|
+
return unique(
|
|
15
|
+
[targetOpenClaw?.configuredPath, ...(targetOpenClaw?.searchedPaths ?? [])]
|
|
16
|
+
.filter((value) => typeof value === "string" && isAbsolutePath(value))
|
|
17
|
+
.sort((left, right) => right.length - left.length),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sanitizeValue(value, sensitivePaths, placeholder) {
|
|
22
|
+
if (typeof value === "string") {
|
|
23
|
+
return sensitivePaths.reduce((result, sensitivePath) => result.replaceAll(sensitivePath, placeholder), value);
|
|
24
|
+
}
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
return value.map((item) => sanitizeValue(item, sensitivePaths, placeholder));
|
|
27
|
+
}
|
|
28
|
+
if (value && typeof value === "object") {
|
|
29
|
+
return Object.fromEntries(
|
|
30
|
+
Object.entries(value).map(([key, entryValue]) => [key, sanitizeValue(entryValue, sensitivePaths, placeholder)]),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isAbsolutePath(value) {
|
|
37
|
+
return path.isAbsolute(value) || /^[A-Za-z]:[\\/]/u.test(value) || value.startsWith("\\\\");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function unique(values) {
|
|
41
|
+
return [...new Set(values)];
|
|
42
|
+
}
|
package/src/report.js
CHANGED
|
@@ -4,6 +4,7 @@ import { renderCompatibilityIssuesReport, renderCompatibilityMarkdownReport } fr
|
|
|
4
4
|
import { buildContractProbes } from "./contract-probes.js";
|
|
5
5
|
import { classifyCompatibilityFixture } from "./fixture-summary.js";
|
|
6
6
|
import { buildIssues, summarizeIssueClasses } from "./issues.js";
|
|
7
|
+
import { sanitizeReportArtifact } from "./report-sanitizer.js";
|
|
7
8
|
|
|
8
9
|
export function buildReport({ config, inspections, failures = [], generatedAt = "deterministic" }) {
|
|
9
10
|
const inspectionById = new Map(inspections.map((inspection) => [inspection.id, inspection]));
|
|
@@ -233,12 +234,13 @@ export async function writeReport(report, options = {}) {
|
|
|
233
234
|
const basename = options.basename ?? "plugin-inspector-report";
|
|
234
235
|
const jsonPath = path.join(outDir, `${basename}.json`);
|
|
235
236
|
const markdownPath = path.join(outDir, `${basename}.md`);
|
|
237
|
+
const artifactReport = sanitizeReportArtifact(report, options);
|
|
236
238
|
|
|
237
239
|
return writeJsonMarkdownArtifacts({
|
|
238
240
|
jsonPath,
|
|
239
241
|
markdownPath,
|
|
240
|
-
json:
|
|
241
|
-
markdown: renderMarkdownReport(
|
|
242
|
+
json: artifactReport,
|
|
243
|
+
markdown: renderMarkdownReport(artifactReport),
|
|
242
244
|
check: options.check,
|
|
243
245
|
});
|
|
244
246
|
}
|
|
@@ -249,6 +251,7 @@ export async function writeCompatibilityReport(report, options = {}) {
|
|
|
249
251
|
const jsonPath = options.jsonPath ?? path.join(outDir, `${basename}.json`);
|
|
250
252
|
const markdownPath = options.markdownPath ?? path.join(outDir, `${basename}.md`);
|
|
251
253
|
const issuesPath = options.issuesPath ?? path.join(outDir, options.issuesBasename ?? "plugin-inspector-issues.md");
|
|
254
|
+
const artifactReport = sanitizeReportArtifact(report, options);
|
|
252
255
|
const markdownOptions = compatibilityRenderOptions(options, {
|
|
253
256
|
title: options.markdownTitle ?? options.title,
|
|
254
257
|
...options.markdownOptions,
|
|
@@ -260,14 +263,16 @@ export async function writeCompatibilityReport(report, options = {}) {
|
|
|
260
263
|
|
|
261
264
|
return writeArtifacts(
|
|
262
265
|
[
|
|
263
|
-
{ name: "jsonPath", path: jsonPath, json:
|
|
264
|
-
{ name: "markdownPath", path: markdownPath, markdown: renderCompatibilityMarkdownReport(
|
|
265
|
-
{ name: "issuesPath", path: issuesPath, markdown: renderCompatibilityIssuesReport(
|
|
266
|
+
{ name: "jsonPath", path: jsonPath, json: artifactReport },
|
|
267
|
+
{ name: "markdownPath", path: markdownPath, markdown: renderCompatibilityMarkdownReport(artifactReport, markdownOptions) },
|
|
268
|
+
{ name: "issuesPath", path: issuesPath, markdown: renderCompatibilityIssuesReport(artifactReport, issuesOptions) },
|
|
266
269
|
],
|
|
267
270
|
{ check: options.check },
|
|
268
271
|
);
|
|
269
272
|
}
|
|
270
273
|
|
|
274
|
+
export { sanitizeReportArtifact };
|
|
275
|
+
|
|
271
276
|
function compatibilityRenderOptions(options, overrides) {
|
|
272
277
|
const renderOptions = {
|
|
273
278
|
formatEvidence: options.formatEvidence,
|
package/src/runtime-profile.js
CHANGED
|
@@ -46,8 +46,8 @@ export async function buildRuntimeProfile(options = {}) {
|
|
|
46
46
|
os: process.platform,
|
|
47
47
|
arch: process.arch,
|
|
48
48
|
node: process.version,
|
|
49
|
-
rssSampler: process.platform === "win32" ? "unavailable" : "ps",
|
|
50
|
-
cpuSampler: process.platform === "win32" ? "unavailable" : "ps-percent",
|
|
49
|
+
rssSampler: process.platform === "win32" ? "unavailable" : "ps-immediate-25ms",
|
|
50
|
+
cpuSampler: process.platform === "win32" ? "unavailable" : "ps-percent-immediate-25ms",
|
|
51
51
|
},
|
|
52
52
|
summary: summarizeProfile(commands),
|
|
53
53
|
groups: summarizeCommandGroups(commands),
|
|
@@ -65,8 +65,8 @@ export function validateRuntimeProfile(profile) {
|
|
|
65
65
|
errors.push(`${command.id}: missing wall time`);
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
-
if (profile.platform?.rssSampler !== "unavailable" && profile.commands.every((command) => command
|
|
69
|
-
errors.push("all commands are missing peak RSS");
|
|
68
|
+
if (profile.platform?.rssSampler !== "unavailable" && profile.commands.every((command) => !hasRssSample(command))) {
|
|
69
|
+
errors.push("all commands are missing peak RSS samples");
|
|
70
70
|
}
|
|
71
71
|
return errors;
|
|
72
72
|
}
|
|
@@ -98,10 +98,17 @@ export function renderRuntimeProfileMarkdown(profile, options = {}) {
|
|
|
98
98
|
[
|
|
99
99
|
["Commands", profile.summary.commandCount],
|
|
100
100
|
["P50 wall time", `${profile.summary.p50WallMs} ms`],
|
|
101
|
-
["P95 wall time", `${profile.summary.p95WallMs} ms`],
|
|
102
|
-
["
|
|
103
|
-
["
|
|
104
|
-
["
|
|
101
|
+
["Command P95 wall time", `${profile.summary.p95WallMs} ms`],
|
|
102
|
+
["Wall time basis", profile.summary.wallTimeBasis ?? "command-median-p95"],
|
|
103
|
+
["Profile samples", profile.summary.sampleCount ?? sampleCount(profile.commands)],
|
|
104
|
+
["RSS samples", profile.summary.rssSampleCount ?? rssSampleCount(profile.commands)],
|
|
105
|
+
["CPU samples", profile.summary.cpuSampleCount ?? cpuSampleCount(profile.commands)],
|
|
106
|
+
["Max peak RSS", formatSampledMetric(profile.summary.maxPeakRssMb, profile.summary.rssSampleCount ?? rssSampleCount(profile.commands))],
|
|
107
|
+
["Max RSS delta", formatSampledMetric(profile.summary.maxRssDeltaMb, profile.summary.rssSampleCount ?? rssSampleCount(profile.commands))],
|
|
108
|
+
[
|
|
109
|
+
"Max CPU estimate",
|
|
110
|
+
formatSampledMetric(profile.summary.maxCpuMsEstimate, profile.summary.cpuSampleCount ?? cpuSampleCount(profile.commands), "ms"),
|
|
111
|
+
],
|
|
105
112
|
["Max harness heap delta", `${profile.summary.maxHarnessHeapDeltaMb} MB`],
|
|
106
113
|
],
|
|
107
114
|
["Metric", "Value"],
|
|
@@ -129,13 +136,14 @@ export function renderRuntimeProfileMarkdown(profile, options = {}) {
|
|
|
129
136
|
command.label,
|
|
130
137
|
`${command.wallMs.median} ms`,
|
|
131
138
|
`${command.wallMs.max} ms`,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
139
|
+
formatSampledMetric(command.peakRssMb.max, command.rssSampleCount),
|
|
140
|
+
formatSampledMetric(command.rssDeltaMb.max, command.rssSampleCount),
|
|
141
|
+
formatSampledMetric(command.cpuMsEstimate.max, command.cpuSampleCount, "ms"),
|
|
135
142
|
`${command.harnessHeapDeltaMb.max} MB`,
|
|
143
|
+
`${command.rssSampleCount ?? 0}/${command.cpuSampleCount ?? 0}`,
|
|
136
144
|
command.exitCodes.join(", "),
|
|
137
145
|
]),
|
|
138
|
-
["ID", "Label", "Median wall", "Max wall", "Max peak RSS", "Max RSS delta", "CPU estimate", "Heap delta", "Exit codes"],
|
|
146
|
+
["ID", "Label", "Median wall", "Max wall", "Max peak RSS", "Max RSS delta", "CPU estimate", "Heap delta", "RSS/CPU samples", "Exit codes"],
|
|
139
147
|
),
|
|
140
148
|
"",
|
|
141
149
|
"## Category Rollups",
|
|
@@ -146,11 +154,12 @@ export function renderRuntimeProfileMarkdown(profile, options = {}) {
|
|
|
146
154
|
group.commandCount,
|
|
147
155
|
`${group.p50WallMs} ms`,
|
|
148
156
|
`${group.p95WallMs} ms`,
|
|
149
|
-
|
|
150
|
-
|
|
157
|
+
formatSampledMetric(group.maxPeakRssMb, group.rssSampleCount),
|
|
158
|
+
formatSampledMetric(group.maxCpuMsEstimate, group.cpuSampleCount, "ms"),
|
|
159
|
+
`${group.rssSampleCount ?? 0}/${group.cpuSampleCount ?? 0}`,
|
|
151
160
|
group.commands.join(", "),
|
|
152
161
|
]),
|
|
153
|
-
["Category", "Commands", "P50 wall", "P95 wall", "Max peak RSS", "CPU estimate", "Command IDs"],
|
|
162
|
+
["Category", "Commands", "P50 wall", "P95 wall", "Max peak RSS", "CPU estimate", "RSS/CPU samples", "Command IDs"],
|
|
154
163
|
),
|
|
155
164
|
].join("\n");
|
|
156
165
|
}
|
|
@@ -189,8 +198,15 @@ function summarizeProfile(commands) {
|
|
|
189
198
|
const maxRssDeltaMb = Math.max(0, ...commands.map((command) => command.rssDeltaMb.max));
|
|
190
199
|
const maxCpuMsEstimate = Math.max(0, ...commands.map((command) => command.cpuMsEstimate.max));
|
|
191
200
|
const maxHarnessHeapDeltaMb = Math.max(0, ...commands.map((command) => command.harnessHeapDeltaMb.max));
|
|
201
|
+
const totalSampleCount = sampleCount(commands);
|
|
202
|
+
const totalRssSampleCount = rssSampleCount(commands);
|
|
203
|
+
const totalCpuSampleCount = cpuSampleCount(commands);
|
|
192
204
|
return {
|
|
193
205
|
commandCount: commands.length,
|
|
206
|
+
sampleCount: totalSampleCount,
|
|
207
|
+
rssSampleCount: totalRssSampleCount,
|
|
208
|
+
cpuSampleCount: totalCpuSampleCount,
|
|
209
|
+
wallTimeBasis: "command-median-p95",
|
|
194
210
|
p50WallMs: percentile(wallTimes, 0.5),
|
|
195
211
|
p95WallMs: percentile(wallTimes, 0.95),
|
|
196
212
|
maxPeakRssMb,
|
|
@@ -206,6 +222,12 @@ function summarizeCommand(command, samples) {
|
|
|
206
222
|
const rssDeltaMb = samples.map((sample) => sample.rssDeltaMb).sort((left, right) => left - right);
|
|
207
223
|
const peakCpuPercent = samples.map((sample) => sample.peakCpuPercent).sort((left, right) => left - right);
|
|
208
224
|
const cpuMsEstimate = samples.map((sample) => sample.cpuMsEstimate).sort((left, right) => left - right);
|
|
225
|
+
const statSampleCount = samples.reduce((sum, sample) => sum + (sample.statSampleCount ?? 0), 0);
|
|
226
|
+
const rssSampleTotal = samples.reduce(
|
|
227
|
+
(sum, sample) => sum + (sample.rssSampleCount ?? (sample.peakRssMb > 0 ? 1 : 0)),
|
|
228
|
+
0,
|
|
229
|
+
);
|
|
230
|
+
const cpuSampleTotal = samples.reduce((sum, sample) => sum + (sample.cpuSampleCount ?? 0), 0);
|
|
209
231
|
const harnessHeapDeltaMb = samples
|
|
210
232
|
.map((sample) => sample.harnessHeapDeltaMb)
|
|
211
233
|
.sort((left, right) => left - right);
|
|
@@ -222,6 +244,9 @@ function summarizeCommand(command, samples) {
|
|
|
222
244
|
peakCpuPercent: summarizeNumbers(peakCpuPercent),
|
|
223
245
|
cpuMsEstimate: summarizeNumbers(cpuMsEstimate),
|
|
224
246
|
harnessHeapDeltaMb: summarizeNumbers(harnessHeapDeltaMb),
|
|
247
|
+
statSampleCount,
|
|
248
|
+
rssSampleCount: rssSampleTotal,
|
|
249
|
+
cpuSampleCount: cpuSampleTotal,
|
|
225
250
|
exitCodes: [...new Set(samples.map((sample) => sample.exitCode))].sort(),
|
|
226
251
|
};
|
|
227
252
|
}
|
|
@@ -244,6 +269,8 @@ function summarizeCommandGroups(commands) {
|
|
|
244
269
|
const cpuMs = categoryCommands
|
|
245
270
|
.flatMap((command) => command.samples.map((sample) => sample.cpuMsEstimate))
|
|
246
271
|
.sort((left, right) => left - right);
|
|
272
|
+
const groupRssSampleCount = rssSampleCount(categoryCommands);
|
|
273
|
+
const groupCpuSampleCount = cpuSampleCount(categoryCommands);
|
|
247
274
|
return {
|
|
248
275
|
category,
|
|
249
276
|
commandCount: categoryCommands.length,
|
|
@@ -251,11 +278,36 @@ function summarizeCommandGroups(commands) {
|
|
|
251
278
|
p95WallMs: percentile(wallTimes, 0.95),
|
|
252
279
|
maxPeakRssMb: peakRss.at(-1) ?? 0,
|
|
253
280
|
maxCpuMsEstimate: cpuMs.at(-1) ?? 0,
|
|
281
|
+
rssSampleCount: groupRssSampleCount,
|
|
282
|
+
cpuSampleCount: groupCpuSampleCount,
|
|
254
283
|
commands: categoryCommands.map((command) => command.id),
|
|
255
284
|
};
|
|
256
285
|
});
|
|
257
286
|
}
|
|
258
287
|
|
|
288
|
+
function hasRssSample(command) {
|
|
289
|
+
return (command.rssSampleCount ?? (command.peakRssMb?.max > 0 ? 1 : 0)) > 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function sampleCount(commands) {
|
|
293
|
+
return commands.reduce((sum, command) => sum + (command.samples?.length ?? 0), 0);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function rssSampleCount(commands) {
|
|
297
|
+
return commands.reduce((sum, command) => sum + (command.rssSampleCount ?? (command.peakRssMb?.max > 0 ? 1 : 0)), 0);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function cpuSampleCount(commands) {
|
|
301
|
+
return commands.reduce((sum, command) => sum + (command.cpuSampleCount ?? 0), 0);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function formatSampledMetric(value, count, unit = "MB") {
|
|
305
|
+
if ((count ?? 0) <= 0) {
|
|
306
|
+
return "n/a";
|
|
307
|
+
}
|
|
308
|
+
return `${value} ${unit}`;
|
|
309
|
+
}
|
|
310
|
+
|
|
259
311
|
function summarizeNumbers(values) {
|
|
260
312
|
return {
|
|
261
313
|
min: values[0],
|
package/src/synthetic-probes.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { renderPaddedMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
|
|
2
2
|
|
|
3
3
|
export const syntheticRegistrationExecutionProfiles = {
|
|
4
|
+
createChatChannelPlugin: {
|
|
5
|
+
mode: "metadata-only",
|
|
6
|
+
callableProperties: [],
|
|
7
|
+
reason: "channel plugin factory metadata is captured before channel runtime execution",
|
|
8
|
+
},
|
|
4
9
|
defineChannelPluginEntry: {
|
|
5
10
|
mode: "metadata-only",
|
|
6
11
|
callableProperties: [],
|
|
@@ -326,6 +331,7 @@ export const defaultSyntheticHookContexts = {
|
|
|
326
331
|
};
|
|
327
332
|
|
|
328
333
|
export const defaultSyntheticRegistrationArguments = {
|
|
334
|
+
createChatChannelPlugin: [{ base: { id: "fixture-channel" }, outbound: { sendText: "function" } }],
|
|
329
335
|
defineChannelPluginEntry: [{ id: "fixture-channel", setup: "function", receive: "function" }],
|
|
330
336
|
definePluginEntry: [{ id: "fixture-plugin", register: "function" }],
|
|
331
337
|
registerChannel: [{ id: "fixture-channel", send: "function", receive: "function" }],
|
package/src/workspace-plan.js
CHANGED
|
@@ -218,7 +218,7 @@ async function buildEntrypointPlan({ fixtureId, entrypoint, packageSummary, pack
|
|
|
218
218
|
const packageManager = detectPackageManager(settings.rootDir, packageDir, packageJson);
|
|
219
219
|
const lockfile = findNearestLockfile(settings.rootDir, packageDir);
|
|
220
220
|
const buildScript = packageJson.scripts?.build;
|
|
221
|
-
const requiredCapabilities = requiredCapabilitiesFor(entrypoint);
|
|
221
|
+
const requiredCapabilities = requiredCapabilitiesFor(entrypoint, packageSummary);
|
|
222
222
|
const loaderStrategy = loaderStrategyFor(entrypoint);
|
|
223
223
|
const blockers = [...entrypoint.blockers];
|
|
224
224
|
const workspacePath = posixJoin(settings.workspaceRoot, fixtureId);
|
|
@@ -342,7 +342,7 @@ function loaderStrategyFor(entrypoint) {
|
|
|
342
342
|
};
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
-
function requiredCapabilitiesFor(entrypoint) {
|
|
345
|
+
function requiredCapabilitiesFor(entrypoint, packageSummary = {}) {
|
|
346
346
|
const capabilities = new Set();
|
|
347
347
|
for (const blocker of entrypoint.blockers) {
|
|
348
348
|
if (blocker.code === "dependency-install-required") {
|
|
@@ -361,7 +361,7 @@ function requiredCapabilitiesFor(entrypoint) {
|
|
|
361
361
|
capabilities.add("side-effect-sandbox");
|
|
362
362
|
}
|
|
363
363
|
}
|
|
364
|
-
if (
|
|
364
|
+
if (hasHostLinkedOpenClawDependency(packageSummary)) {
|
|
365
365
|
capabilities.add("target-openclaw-link");
|
|
366
366
|
}
|
|
367
367
|
capabilities.add("capture-shim");
|
|
@@ -369,6 +369,14 @@ function requiredCapabilitiesFor(entrypoint) {
|
|
|
369
369
|
return [...capabilities].sort();
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
+
function hasHostLinkedOpenClawDependency(packageSummary) {
|
|
373
|
+
return [
|
|
374
|
+
...(packageSummary.dependencies ?? []),
|
|
375
|
+
...(packageSummary.peerDependencies ?? []),
|
|
376
|
+
...(packageSummary.optionalDependencies ?? []),
|
|
377
|
+
].includes("openclaw");
|
|
378
|
+
}
|
|
379
|
+
|
|
372
380
|
function detectPackageManager(rootDir, packageDir, packageJson) {
|
|
373
381
|
const declared = typeof packageJson.packageManager === "string" ? packageJson.packageManager.split("@")[0] : null;
|
|
374
382
|
if (declared) {
|