feat: attribute agent cli pre-provider spans

This commit is contained in:
Shakker 2026-05-06 15:03:33 +01:00
parent 2b5bddff4a
commit 75f60628ff
No known key found for this signature in database
7 changed files with 736 additions and 395 deletions

View File

@ -0,0 +1,57 @@
import {
buildPreProviderAttribution,
preProviderMarkdownRows,
summarizePreProviderAttributions
} from "./pre-provider-attribution.mjs";
export const AGENT_CLI_PRE_PROVIDER_ATTRIBUTION_SCHEMA = "kova.agentCliPreProviderAttribution.v1";
export const AGENT_CLI_PRE_PROVIDER_SUMMARY_SCHEMA = "kova.agentCliPreProviderAttributionSummary.v1";
export function buildAgentCliPreProviderAttribution({
label,
phaseId,
activeStartedAtEpochMs,
activeFinishedAtEpochMs,
attribution,
timelineSummary
}) {
return buildPreProviderAttribution({
schemaVersion: AGENT_CLI_PRE_PROVIDER_ATTRIBUTION_SCHEMA,
label,
phaseId,
activeStartedAtEpochMs,
activeFinishedAtEpochMs,
attribution,
timelineSummary,
isAttributedSpanName: isAgentCliAttributedSpanName,
missingEventsError: "timeline contains no agent CLI attribution events"
});
}
export function summarizeAgentCliPreProviderAttributions(turns) {
return summarizePreProviderAttributions({
schemaVersion: AGENT_CLI_PRE_PROVIDER_SUMMARY_SCHEMA,
turns,
fieldName: "agentCliPreProviderAttribution"
});
}
export function agentCliPreProviderMarkdownRows(turns) {
return preProviderMarkdownRows({
title: "Agent CLI pre-provider attribution",
turns,
fieldName: "agentCliPreProviderAttribution"
});
}
function isAgentCliAttributedSpanName(name) {
const text = String(name ?? "");
return text === "agent.prepare" ||
text === "plugins.metadata.scan" ||
text === "runtimeDeps.stage" ||
text === "channel.capabilities" ||
text === "models.catalog" ||
text.startsWith("models.catalog.") ||
text.startsWith("models.discovery") ||
text.startsWith("channel.plugin.");
}

View File

@ -1,3 +1,10 @@
import {
attributedSpanIntervals as collectAttributedSpanIntervals,
buildPreProviderAttribution,
preProviderMarkdownRows,
summarizePreProviderAttributions
} from "./pre-provider-attribution.mjs";
export const DASHBOARD_PRE_PROVIDER_ATTRIBUTION_SCHEMA = "kova.dashboardPreProviderAttribution.v1";
export const DASHBOARD_PRE_PROVIDER_SUMMARY_SCHEMA = "kova.dashboardPreProviderAttributionSummary.v1";
@ -9,323 +16,37 @@ export function buildDashboardPreProviderAttribution({
attribution,
timelineSummary
}) {
const artifacts = timelineArtifacts(timelineSummary);
const events = attributionEvents(timelineSummary);
const providerBoundaryEpochMs = numberOrNull(attribution?.firstProviderRequestAtEpochMs);
const windowStartEpochMs = numberOrNull(activeStartedAtEpochMs ?? attribution?.commandStartedAtEpochMs);
const activeEndEpochMs = numberOrNull(activeFinishedAtEpochMs ?? attribution?.commandFinishedAtEpochMs);
const windowEndEpochMs = providerBoundaryEpochMs;
const preProviderMs = numberOrNull(attribution?.preProviderMs) ??
durationBetween(windowStartEpochMs, windowEndEpochMs);
const base = {
return buildPreProviderAttribution({
schemaVersion: DASHBOARD_PRE_PROVIDER_ATTRIBUTION_SCHEMA,
available: false,
label: label ?? null,
phaseId: phaseId ?? null,
timelineAvailable: timelineSummary?.available === true,
timelineArtifacts: artifacts,
eventCount: events.length,
window: {
startEpochMs: windowStartEpochMs,
startAt: isoOrNull(windowStartEpochMs),
endEpochMs: windowEndEpochMs,
endAt: isoOrNull(windowEndEpochMs),
durationMs: preProviderMs
},
activeWindow: {
startEpochMs: windowStartEpochMs,
startAt: isoOrNull(windowStartEpochMs),
endEpochMs: activeEndEpochMs,
endAt: isoOrNull(activeEndEpochMs),
durationMs: durationBetween(windowStartEpochMs, activeEndEpochMs)
},
providerBoundary: {
firstRequestAtEpochMs: providerBoundaryEpochMs,
firstRequestAt: isoOrNull(providerBoundaryEpochMs),
source: providerBoundaryEpochMs === null ? null : "provider-evidence"
},
provider: summarizeProviderEvents(events, providerBoundaryEpochMs, activeEndEpochMs, attribution),
spanSummaries: [],
knownAttributedMs: null,
unattributedMs: preProviderMs,
coverageRatio: null,
error: null
};
if (timelineSummary?.available !== true) {
return {
...base,
error: "OpenClaw diagnostics timeline unavailable"
};
}
if (events.length === 0) {
return {
...base,
error: "timeline contains no dashboard turn attribution events"
};
}
if (windowStartEpochMs === null || windowEndEpochMs === null || preProviderMs === null || windowEndEpochMs < windowStartEpochMs) {
return {
...base,
error: "pre-provider window boundary unavailable"
};
}
const intervals = attributedSpanIntervals(events)
.map((span) => clipSpanToWindow(span, windowStartEpochMs, windowEndEpochMs))
.filter(Boolean);
const spanSummaries = summarizeAttributedSpans(intervals);
const knownAttributedMs = round(unionDuration(intervals));
const unattributedMs = round(Math.max(0, preProviderMs - knownAttributedMs));
return {
...base,
available: true,
spanSummaries,
knownAttributedMs,
unattributedMs,
coverageRatio: preProviderMs > 0 ? round(knownAttributedMs / preProviderMs) : null,
error: null
};
label,
phaseId,
activeStartedAtEpochMs,
activeFinishedAtEpochMs,
attribution,
timelineSummary,
isAttributedSpanName: isDashboardAttributedSpanName,
missingEventsError: "timeline contains no dashboard turn attribution events"
});
}
export function summarizeDashboardPreProviderAttributions(turns) {
const entries = (turns ?? [])
.map((turn) => turn.dashboardPreProviderAttribution)
.filter(Boolean);
const cold = summarizeLabeledAttribution(entries, "cold");
const warm = summarizeLabeledAttribution(entries, "warm");
return {
return summarizePreProviderAttributions({
schemaVersion: DASHBOARD_PRE_PROVIDER_SUMMARY_SCHEMA,
available: entries.some((entry) => entry.available === true),
count: entries.length,
cold,
warm,
spanMedians: summarizeSpanMedians(entries),
timelineArtifacts: unique(entries.flatMap((entry) => entry.timelineArtifacts ?? []))
};
turns,
fieldName: "dashboardPreProviderAttribution"
});
}
export function dashboardPreProviderMarkdownRows(turns) {
const attributions = (turns ?? [])
.map((turn) => turn.dashboardPreProviderAttribution)
.filter(Boolean);
if (attributions.length === 0) {
return [];
}
const lines = [
"- Dashboard pre-provider attribution:",
"",
" | turn | pre-provider | known | unattributed | provider | timeline |",
" |---|---:|---:|---:|---:|---|"
];
for (const item of attributions) {
const timeline = item.timelineArtifacts?.[0] ?? (item.timelineAvailable ? "available" : "missing");
lines.push(
` | ${item.label ?? "turn"} | ${formatMs(item.window?.durationMs)} | ${formatMs(item.knownAttributedMs)} | ${formatMs(item.unattributedMs)} | ${formatMs(item.provider?.totalDurationMs)} | ${timeline} |`
);
}
const spanRows = attributions.flatMap((item) =>
(item.spanSummaries ?? []).slice(0, 6).map((span) => ({ turn: item.label ?? "turn", ...span }))
);
if (spanRows.length > 0) {
lines.push("");
lines.push(" | turn | span | count | errors | clipped | max |");
lines.push(" |---|---|---:|---:|---:|---:|");
for (const span of spanRows.slice(0, 12)) {
lines.push(
` | ${span.turn} | \`${span.name}\` | ${span.count} | ${span.errorCount} | ${formatMs(span.totalClippedDurationMs)} | ${formatMs(span.maxClippedDurationMs)} |`
);
}
}
return lines;
return preProviderMarkdownRows({
title: "Dashboard pre-provider attribution",
turns,
fieldName: "dashboardPreProviderAttribution"
});
}
export function attributedSpanIntervals(events) {
const startsById = new Map();
const intervals = [];
for (const event of events ?? []) {
if (event?.type === "span.start" && isDashboardAttributedSpanName(event.name)) {
const key = spanKey(event);
if (key) {
startsById.set(key, event);
}
continue;
}
if ((event?.type === "span.end" || event?.type === "span.error") && isDashboardAttributedSpanName(event.name)) {
const terminal = spanIntervalFromTerminal(event, startsById.get(spanKey(event)));
if (terminal) {
intervals.push(terminal);
}
}
}
return intervals;
}
function spanIntervalFromTerminal(event, startEvent) {
const endEpochMs = eventEpochMs(event);
const durationMs = numberOrNull(event.durationMs) ?? durationBetween(eventEpochMs(startEvent), endEpochMs);
const startEpochMs = eventEpochMs(startEvent) ??
(endEpochMs !== null && durationMs !== null ? endEpochMs - durationMs : null);
if (startEpochMs === null || endEpochMs === null || endEpochMs < startEpochMs) {
return null;
}
return {
name: event.name,
type: event.type,
startEpochMs,
endEpochMs,
durationMs: round(endEpochMs - startEpochMs),
rawDurationMs: durationMs,
spanId: event.spanId ?? null,
phase: event.phase ?? startEvent?.phase ?? null,
errorName: event.errorName ?? null,
errorMessage: event.errorMessage ?? null
};
}
function clipSpanToWindow(span, windowStartEpochMs, windowEndEpochMs) {
const startEpochMs = Math.max(span.startEpochMs, windowStartEpochMs);
const endEpochMs = Math.min(span.endEpochMs, windowEndEpochMs);
if (endEpochMs <= startEpochMs) {
return null;
}
return {
...span,
clippedStartEpochMs: startEpochMs,
clippedEndEpochMs: endEpochMs,
clippedDurationMs: round(endEpochMs - startEpochMs)
};
}
function summarizeAttributedSpans(intervals) {
const byName = new Map();
for (const interval of intervals) {
const current = byName.get(interval.name) ?? {
name: interval.name,
count: 0,
errorCount: 0,
totalClippedDurationMs: 0,
maxClippedDurationMs: null,
totalRawDurationMs: 0,
maxRawDurationMs: null
};
current.count += 1;
if (interval.type === "span.error") {
current.errorCount += 1;
}
current.totalClippedDurationMs = round(current.totalClippedDurationMs + interval.clippedDurationMs);
current.maxClippedDurationMs = maxNullable(current.maxClippedDurationMs, interval.clippedDurationMs);
if (typeof interval.rawDurationMs === "number") {
current.totalRawDurationMs = round(current.totalRawDurationMs + interval.rawDurationMs);
current.maxRawDurationMs = maxNullable(current.maxRawDurationMs, interval.rawDurationMs);
}
byName.set(interval.name, current);
}
return [...byName.values()].toSorted((left, right) =>
(right.totalClippedDurationMs - left.totalClippedDurationMs) ||
left.name.localeCompare(right.name)
);
}
function summarizeProviderEvents(events, providerBoundaryEpochMs, activeFinishedAtEpochMs, attribution) {
const providerEvents = (events ?? [])
.filter((event) => event?.type === "provider.request" || event?.name === "provider.request")
.map((event) => {
const startEpochMs = numberOrNull(event.receivedAtEpochMs) ?? eventEpochMs(event);
const durationMs = numberOrNull(event.durationMs);
const endEpochMs = numberOrNull(event.respondedAtEpochMs) ??
(startEpochMs !== null && durationMs !== null ? startEpochMs + durationMs : null);
return { event, startEpochMs, endEpochMs, durationMs: durationMs ?? durationBetween(startEpochMs, endEpochMs) };
})
.filter((event) =>
event.startEpochMs !== null &&
(providerBoundaryEpochMs === null || event.startEpochMs >= providerBoundaryEpochMs) &&
(activeFinishedAtEpochMs === null || event.startEpochMs <= activeFinishedAtEpochMs)
);
const durations = providerEvents.map((event) => event.durationMs).filter(isNumber);
return {
requestCount: providerEvents.length,
timelineTotalDurationMs: round(durations.reduce((sum, value) => sum + value, 0)),
timelineMaxDurationMs: durations.length > 0 ? Math.max(...durations) : null,
totalDurationMs: numberOrNull(attribution?.providerFinalMs) ??
round(durations.reduce((sum, value) => sum + value, 0)),
firstByteLatencyMs: numberOrNull(attribution?.firstByteLatencyMs),
firstChunkLatencyMs: numberOrNull(attribution?.firstChunkLatencyMs)
};
}
function summarizeLabeledAttribution(entries, label) {
const values = entries.filter((entry) => entry.label === label);
return {
count: values.length,
preProviderMs: summarizeValues(values.map((entry) => entry.window?.durationMs)),
knownAttributedMs: summarizeValues(values.map((entry) => entry.knownAttributedMs)),
unattributedMs: summarizeValues(values.map((entry) => entry.unattributedMs)),
coverageRatio: summarizeValues(values.map((entry) => entry.coverageRatio))
};
}
function summarizeSpanMedians(entries) {
const byName = new Map();
for (const entry of entries) {
for (const span of entry.spanSummaries ?? []) {
const current = byName.get(span.name) ?? [];
current.push(span.totalClippedDurationMs);
byName.set(span.name, current);
}
}
return [...byName.entries()]
.map(([name, values]) => ({
name,
medianClippedDurationMs: summarizeValues(values).median,
sampleCount: values.length
}))
.toSorted((left, right) => (right.medianClippedDurationMs ?? 0) - (left.medianClippedDurationMs ?? 0));
}
function summarizeValues(values) {
const sorted = values.filter(isNumber).toSorted((left, right) => left - right);
if (sorted.length === 0) {
return { count: 0, median: null, min: null, max: null };
}
return {
count: sorted.length,
median: round(percentile(sorted, 50)),
min: round(sorted[0]),
max: round(sorted.at(-1))
};
}
function unionDuration(intervals) {
const sorted = intervals
.filter((interval) => isNumber(interval.clippedStartEpochMs) && isNumber(interval.clippedEndEpochMs))
.toSorted((left, right) => left.clippedStartEpochMs - right.clippedStartEpochMs);
let total = 0;
let currentStart = null;
let currentEnd = null;
for (const interval of sorted) {
if (currentStart === null) {
currentStart = interval.clippedStartEpochMs;
currentEnd = interval.clippedEndEpochMs;
continue;
}
if (interval.clippedStartEpochMs <= currentEnd) {
currentEnd = Math.max(currentEnd, interval.clippedEndEpochMs);
continue;
}
total += currentEnd - currentStart;
currentStart = interval.clippedStartEpochMs;
currentEnd = interval.clippedEndEpochMs;
}
if (currentStart !== null) {
total += currentEnd - currentStart;
}
return total;
return collectAttributedSpanIntervals(events, isDashboardAttributedSpanName);
}
function isDashboardAttributedSpanName(name) {
@ -334,86 +55,3 @@ function isDashboardAttributedSpanName(name) {
text.startsWith("auto_reply") ||
text.startsWith("reply.");
}
function attributionEvents(timelineSummary) {
return Array.isArray(timelineSummary?.turnAttributionEvents) && timelineSummary.turnAttributionEvents.length > 0
? timelineSummary.turnAttributionEvents
: (Array.isArray(timelineSummary?.events) ? timelineSummary.events : []);
}
function timelineArtifacts(timelineSummary) {
return unique([
...(timelineSummary?.artifacts ?? []),
...(timelineSummary?.timelineArtifacts ?? [])
].filter(Boolean));
}
function spanKey(event) {
return event?.spanId === undefined || event?.spanId === null || String(event.spanId).length === 0
? null
: String(event.spanId);
}
function eventEpochMs(event) {
const direct = numberOrNull(event?.timestampEpochMs ?? event?.timeEpochMs);
if (direct !== null) {
return direct;
}
const parsed = Date.parse(event?.timestamp ?? event?.time ?? "");
return Number.isFinite(parsed) ? parsed : null;
}
function durationBetween(startEpochMs, endEpochMs) {
return isNumber(startEpochMs) && isNumber(endEpochMs) && endEpochMs >= startEpochMs
? round(endEpochMs - startEpochMs)
: null;
}
function percentile(sortedValues, percentileValue) {
if (sortedValues.length === 1) {
return sortedValues[0];
}
const position = (percentileValue / 100) * (sortedValues.length - 1);
const lower = Math.floor(position);
const upper = Math.ceil(position);
if (lower === upper) {
return sortedValues[lower];
}
const weight = position - lower;
return sortedValues[lower] * (1 - weight) + sortedValues[upper] * weight;
}
function maxNullable(left, right) {
if (!isNumber(right)) {
return left;
}
return isNumber(left) ? Math.max(left, right) : right;
}
function numberOrNull(value) {
if (value === null || value === undefined || value === "") {
return null;
}
const number = Number(value);
return Number.isFinite(number) ? round(number) : null;
}
function isoOrNull(epochMs) {
return isNumber(epochMs) ? new Date(epochMs).toISOString() : null;
}
function formatMs(value) {
return isNumber(value) ? `${value} ms` : "unknown";
}
function unique(values) {
return [...new Set(values)];
}
function isNumber(value) {
return typeof value === "number" && Number.isFinite(value);
}
function round(value) {
return Math.round(value * 100) / 100;
}

View File

@ -0,0 +1,412 @@
export function buildPreProviderAttribution({
schemaVersion,
label,
phaseId,
activeStartedAtEpochMs,
activeFinishedAtEpochMs,
attribution,
timelineSummary,
isAttributedSpanName,
missingEventsError
}) {
const artifacts = timelineArtifacts(timelineSummary);
const events = attributionEvents(timelineSummary);
const providerBoundaryEpochMs = numberOrNull(attribution?.firstProviderRequestAtEpochMs);
const windowStartEpochMs = numberOrNull(activeStartedAtEpochMs ?? attribution?.commandStartedAtEpochMs);
const activeEndEpochMs = numberOrNull(activeFinishedAtEpochMs ?? attribution?.commandFinishedAtEpochMs);
const windowEndEpochMs = providerBoundaryEpochMs;
const preProviderMs = numberOrNull(attribution?.preProviderMs) ??
durationBetween(windowStartEpochMs, windowEndEpochMs);
const base = {
schemaVersion,
available: false,
label: label ?? null,
phaseId: phaseId ?? null,
timelineAvailable: timelineSummary?.available === true,
timelineArtifacts: artifacts,
eventCount: events.length,
window: {
startEpochMs: windowStartEpochMs,
startAt: isoOrNull(windowStartEpochMs),
endEpochMs: windowEndEpochMs,
endAt: isoOrNull(windowEndEpochMs),
durationMs: preProviderMs
},
activeWindow: {
startEpochMs: windowStartEpochMs,
startAt: isoOrNull(windowStartEpochMs),
endEpochMs: activeEndEpochMs,
endAt: isoOrNull(activeEndEpochMs),
durationMs: durationBetween(windowStartEpochMs, activeEndEpochMs)
},
providerBoundary: {
firstRequestAtEpochMs: providerBoundaryEpochMs,
firstRequestAt: isoOrNull(providerBoundaryEpochMs),
source: providerBoundaryEpochMs === null ? null : "provider-evidence"
},
provider: summarizeProviderEvents(events, providerBoundaryEpochMs, activeEndEpochMs, attribution),
spanSummaries: [],
knownAttributedMs: null,
unattributedMs: preProviderMs,
coverageRatio: null,
error: null
};
if (timelineSummary?.available !== true) {
return {
...base,
error: "OpenClaw diagnostics timeline unavailable"
};
}
if (events.length === 0) {
return {
...base,
error: missingEventsError ?? "timeline contains no pre-provider attribution events"
};
}
if (windowStartEpochMs === null || windowEndEpochMs === null || preProviderMs === null || windowEndEpochMs < windowStartEpochMs) {
return {
...base,
error: "pre-provider window boundary unavailable"
};
}
const intervals = attributedSpanIntervals(events, isAttributedSpanName)
.map((span) => clipSpanToWindow(span, windowStartEpochMs, windowEndEpochMs))
.filter(Boolean);
const spanSummaries = summarizeAttributedSpans(intervals);
const knownAttributedMs = round(unionDuration(intervals));
const unattributedMs = round(Math.max(0, preProviderMs - knownAttributedMs));
return {
...base,
available: true,
spanSummaries,
knownAttributedMs,
unattributedMs,
coverageRatio: preProviderMs > 0 ? round(knownAttributedMs / preProviderMs) : null,
error: null
};
}
export function summarizePreProviderAttributions({ schemaVersion, turns, fieldName }) {
const entries = (turns ?? [])
.map((turn) => turn?.[fieldName])
.filter(Boolean);
const cold = summarizeLabeledAttribution(entries, "cold");
const warm = summarizeLabeledAttribution(entries, "warm");
return {
schemaVersion,
available: entries.some((entry) => entry.available === true),
count: entries.length,
cold,
warm,
spanMedians: summarizeSpanMedians(entries),
timelineArtifacts: unique(entries.flatMap((entry) => entry.timelineArtifacts ?? []))
};
}
export function preProviderMarkdownRows({ title, turns, fieldName }) {
const attributions = (turns ?? [])
.map((turn) => turn?.[fieldName])
.filter(Boolean);
if (attributions.length === 0) {
return [];
}
const lines = [
`- ${title}:`,
"",
" | turn | pre-provider | known | unattributed | provider | timeline |",
" |---|---:|---:|---:|---:|---|"
];
for (const item of attributions) {
const timeline = item.timelineArtifacts?.[0] ?? (item.timelineAvailable ? "available" : "missing");
lines.push(
` | ${item.label ?? "turn"} | ${formatMs(item.window?.durationMs)} | ${formatMs(item.knownAttributedMs)} | ${formatMs(item.unattributedMs)} | ${formatMs(item.provider?.totalDurationMs)} | ${timeline} |`
);
}
const spanRows = attributions.flatMap((item) =>
(item.spanSummaries ?? []).slice(0, 6).map((span) => ({ turn: item.label ?? "turn", ...span }))
);
if (spanRows.length > 0) {
lines.push("");
lines.push(" | turn | span | count | errors | clipped | max |");
lines.push(" |---|---|---:|---:|---:|---:|");
for (const span of spanRows.slice(0, 12)) {
lines.push(
` | ${span.turn} | \`${span.name}\` | ${span.count} | ${span.errorCount} | ${formatMs(span.totalClippedDurationMs)} | ${formatMs(span.maxClippedDurationMs)} |`
);
}
}
return lines;
}
export function attributedSpanIntervals(events, isAttributedSpanName) {
const startsById = new Map();
const intervals = [];
for (const event of events ?? []) {
if (event?.type === "span.start" && isAttributedSpanName(event.name)) {
const key = spanKey(event);
if (key) {
startsById.set(key, event);
}
continue;
}
if ((event?.type === "span.end" || event?.type === "span.error") && isAttributedSpanName(event.name)) {
const terminal = spanIntervalFromTerminal(event, startsById.get(spanKey(event)));
if (terminal) {
intervals.push(terminal);
}
}
}
return intervals;
}
function spanIntervalFromTerminal(event, startEvent) {
const endEpochMs = eventEpochMs(event);
const durationMs = numberOrNull(event.durationMs) ?? durationBetween(eventEpochMs(startEvent), endEpochMs);
const startEpochMs = eventEpochMs(startEvent) ??
(endEpochMs !== null && durationMs !== null ? endEpochMs - durationMs : null);
if (startEpochMs === null || endEpochMs === null || endEpochMs < startEpochMs) {
return null;
}
return {
name: event.name,
type: event.type,
startEpochMs,
endEpochMs,
durationMs: round(endEpochMs - startEpochMs),
rawDurationMs: durationMs,
spanId: event.spanId ?? null,
phase: event.phase ?? startEvent?.phase ?? null,
errorName: event.errorName ?? null,
errorMessage: event.errorMessage ?? null
};
}
function clipSpanToWindow(span, windowStartEpochMs, windowEndEpochMs) {
const startEpochMs = Math.max(span.startEpochMs, windowStartEpochMs);
const endEpochMs = Math.min(span.endEpochMs, windowEndEpochMs);
if (endEpochMs <= startEpochMs) {
return null;
}
return {
...span,
clippedStartEpochMs: startEpochMs,
clippedEndEpochMs: endEpochMs,
clippedDurationMs: round(endEpochMs - startEpochMs)
};
}
function summarizeAttributedSpans(intervals) {
const byName = new Map();
for (const interval of intervals) {
const current = byName.get(interval.name) ?? {
name: interval.name,
count: 0,
errorCount: 0,
totalClippedDurationMs: 0,
maxClippedDurationMs: null,
totalRawDurationMs: 0,
maxRawDurationMs: null
};
current.count += 1;
if (interval.type === "span.error") {
current.errorCount += 1;
}
current.totalClippedDurationMs = round(current.totalClippedDurationMs + interval.clippedDurationMs);
current.maxClippedDurationMs = maxNullable(current.maxClippedDurationMs, interval.clippedDurationMs);
if (typeof interval.rawDurationMs === "number") {
current.totalRawDurationMs = round(current.totalRawDurationMs + interval.rawDurationMs);
current.maxRawDurationMs = maxNullable(current.maxRawDurationMs, interval.rawDurationMs);
}
byName.set(interval.name, current);
}
return [...byName.values()].toSorted((left, right) =>
(right.totalClippedDurationMs - left.totalClippedDurationMs) ||
left.name.localeCompare(right.name)
);
}
function summarizeProviderEvents(events, providerBoundaryEpochMs, activeFinishedAtEpochMs, attribution) {
const providerEvents = (events ?? [])
.filter((event) => event?.type === "provider.request" || event?.name === "provider.request")
.map((event) => {
const startEpochMs = numberOrNull(event.receivedAtEpochMs) ?? eventEpochMs(event);
const durationMs = numberOrNull(event.durationMs);
const endEpochMs = numberOrNull(event.respondedAtEpochMs) ??
(startEpochMs !== null && durationMs !== null ? startEpochMs + durationMs : null);
return { event, startEpochMs, endEpochMs, durationMs: durationMs ?? durationBetween(startEpochMs, endEpochMs) };
})
.filter((event) =>
event.startEpochMs !== null &&
(providerBoundaryEpochMs === null || event.startEpochMs >= providerBoundaryEpochMs) &&
(activeFinishedAtEpochMs === null || event.startEpochMs <= activeFinishedAtEpochMs)
);
const durations = providerEvents.map((event) => event.durationMs).filter(isNumber);
return {
requestCount: providerEvents.length,
timelineTotalDurationMs: round(durations.reduce((sum, value) => sum + value, 0)),
timelineMaxDurationMs: durations.length > 0 ? Math.max(...durations) : null,
totalDurationMs: numberOrNull(attribution?.providerFinalMs) ??
round(durations.reduce((sum, value) => sum + value, 0)),
firstByteLatencyMs: numberOrNull(attribution?.firstByteLatencyMs),
firstChunkLatencyMs: numberOrNull(attribution?.firstChunkLatencyMs)
};
}
function summarizeLabeledAttribution(entries, label) {
const values = entries.filter((entry) => entry.label === label);
return {
count: values.length,
preProviderMs: summarizeValues(values.map((entry) => entry.window?.durationMs)),
knownAttributedMs: summarizeValues(values.map((entry) => entry.knownAttributedMs)),
unattributedMs: summarizeValues(values.map((entry) => entry.unattributedMs)),
coverageRatio: summarizeValues(values.map((entry) => entry.coverageRatio))
};
}
function summarizeSpanMedians(entries) {
const byName = new Map();
for (const entry of entries) {
for (const span of entry.spanSummaries ?? []) {
const current = byName.get(span.name) ?? [];
current.push(span.totalClippedDurationMs);
byName.set(span.name, current);
}
}
return [...byName.entries()]
.map(([name, values]) => ({
name,
medianClippedDurationMs: summarizeValues(values).median,
sampleCount: values.length
}))
.toSorted((left, right) => (right.medianClippedDurationMs ?? 0) - (left.medianClippedDurationMs ?? 0));
}
function summarizeValues(values) {
const sorted = values.filter(isNumber).toSorted((left, right) => left - right);
if (sorted.length === 0) {
return { count: 0, median: null, min: null, max: null };
}
return {
count: sorted.length,
median: round(percentile(sorted, 50)),
min: round(sorted[0]),
max: round(sorted.at(-1))
};
}
function unionDuration(intervals) {
const sorted = intervals
.filter((interval) => isNumber(interval.clippedStartEpochMs) && isNumber(interval.clippedEndEpochMs))
.toSorted((left, right) => left.clippedStartEpochMs - right.clippedStartEpochMs);
let total = 0;
let currentStart = null;
let currentEnd = null;
for (const interval of sorted) {
if (currentStart === null) {
currentStart = interval.clippedStartEpochMs;
currentEnd = interval.clippedEndEpochMs;
continue;
}
if (interval.clippedStartEpochMs <= currentEnd) {
currentEnd = Math.max(currentEnd, interval.clippedEndEpochMs);
continue;
}
total += currentEnd - currentStart;
currentStart = interval.clippedStartEpochMs;
currentEnd = interval.clippedEndEpochMs;
}
if (currentStart !== null) {
total += currentEnd - currentStart;
}
return total;
}
function attributionEvents(timelineSummary) {
return Array.isArray(timelineSummary?.turnAttributionEvents) && timelineSummary.turnAttributionEvents.length > 0
? timelineSummary.turnAttributionEvents
: (Array.isArray(timelineSummary?.events) ? timelineSummary.events : []);
}
function timelineArtifacts(timelineSummary) {
return unique([
...(timelineSummary?.artifacts ?? []),
...(timelineSummary?.timelineArtifacts ?? [])
].filter(Boolean));
}
function spanKey(event) {
return event?.spanId === undefined || event?.spanId === null || String(event.spanId).length === 0
? null
: String(event.spanId);
}
function eventEpochMs(event) {
const direct = numberOrNull(event?.timestampEpochMs ?? event?.timeEpochMs);
if (direct !== null) {
return direct;
}
const parsed = Date.parse(event?.timestamp ?? event?.time ?? "");
return Number.isFinite(parsed) ? parsed : null;
}
function durationBetween(startEpochMs, endEpochMs) {
return isNumber(startEpochMs) && isNumber(endEpochMs) && endEpochMs >= startEpochMs
? round(endEpochMs - startEpochMs)
: null;
}
function percentile(sortedValues, percentileValue) {
if (sortedValues.length === 1) {
return sortedValues[0];
}
const position = (percentileValue / 100) * (sortedValues.length - 1);
const lower = Math.floor(position);
const upper = Math.ceil(position);
if (lower === upper) {
return sortedValues[lower];
}
const weight = position - lower;
return sortedValues[lower] * (1 - weight) + sortedValues[upper] * weight;
}
function maxNullable(left, right) {
if (!isNumber(right)) {
return left;
}
return isNumber(left) ? Math.max(left, right) : right;
}
function numberOrNull(value) {
if (value === null || value === undefined || value === "") {
return null;
}
const number = Number(value);
return Number.isFinite(number) ? round(number) : null;
}
function isoOrNull(epochMs) {
return isNumber(epochMs) ? new Date(epochMs).toISOString() : null;
}
function formatMs(value) {
return isNumber(value) ? `${value} ms` : "unknown";
}
function unique(values) {
return [...new Set(values)];
}
function isNumber(value) {
return typeof value === "number" && Number.isFinite(value);
}
function round(value) {
return Math.round(value * 100) / 100;
}

View File

@ -447,9 +447,18 @@ function isTurnAttributionEvent(event) {
}
return event.name === "plugins.metadata.scan" ||
event.name === "provider.request" ||
event.name === "agent.prepare" ||
event.name === "agent.turn" ||
event.name === "agent.cleanup" ||
event.name === "runtimeDeps.stage" ||
event.name === "channel.capabilities" ||
event.name === "models.catalog" ||
event.name === "auto_reply" ||
event.name.startsWith("auto_reply.") ||
event.name.startsWith("gateway.chat_send") ||
event.name.startsWith("models.catalog.") ||
event.name.startsWith("models.discovery") ||
event.name.startsWith("channel.plugin.") ||
event.name.startsWith("reply.");
}

View File

@ -1,4 +1,8 @@
import { buildAgentTurnBreakdown } from "./collectors/agent-turns.mjs";
import {
buildAgentCliPreProviderAttribution,
summarizeAgentCliPreProviderAttributions
} from "./collectors/agent-cli-attribution.mjs";
import {
buildDashboardPreProviderAttribution,
summarizeDashboardPreProviderAttributions
@ -82,6 +86,11 @@ export function evaluateRecord(record, scenario, options = {}) {
const agentTurnStats = summarizeAgentTurnStats(agentTurns);
const agentTurnDiagnostics = summarizeAgentTurnDiagnostics(agentTurns);
const dashboardPreProviderAttribution = summarizeDashboardPreProviderAttributions(agentTurns);
const agentCliPreProviderAttribution = summarizeAgentCliPreProviderAttributions(agentTurns);
const turnPreProviderAttribution = preferredPreProviderAttributionSummary(
dashboardPreProviderAttribution,
agentCliPreProviderAttribution
);
const agentTurnMs = maxTurnDuration(agentTurns);
const agentResponseOk = agentTurns.length === 0 ? null : agentTurns.every((turn) => turn.responseOk === true);
const agentProviderSimulation = evaluateProviderSimulation({ turns: agentTurns, scenario, record, thresholds });
@ -760,12 +769,13 @@ export function evaluateRecord(record, scenario, options = {}) {
agentSessionPollCount: agentTurnDiagnostics.sessionPollCount,
agentSessionPollErrorCount: agentTurnDiagnostics.sessionPollErrorCount,
dashboardPreProviderAttribution,
coldPreProviderAttributedMs: dashboardPreProviderAttribution.cold.knownAttributedMs.median,
warmPreProviderAttributedMs: dashboardPreProviderAttribution.warm.knownAttributedMs.median,
coldPreProviderUnattributedMs: dashboardPreProviderAttribution.cold.unattributedMs.median,
warmPreProviderUnattributedMs: dashboardPreProviderAttribution.warm.unattributedMs.median,
coldPreProviderAttributionCoverage: dashboardPreProviderAttribution.cold.coverageRatio.median,
warmPreProviderAttributionCoverage: dashboardPreProviderAttribution.warm.coverageRatio.median,
agentCliPreProviderAttribution,
coldPreProviderAttributedMs: turnPreProviderAttribution.cold.knownAttributedMs.median,
warmPreProviderAttributedMs: turnPreProviderAttribution.warm.knownAttributedMs.median,
coldPreProviderUnattributedMs: turnPreProviderAttribution.cold.unattributedMs.median,
warmPreProviderUnattributedMs: turnPreProviderAttribution.warm.unattributedMs.median,
coldPreProviderAttributionCoverage: turnPreProviderAttribution.cold.coverageRatio.median,
warmPreProviderAttributionCoverage: turnPreProviderAttribution.warm.coverageRatio.median,
coldAgentTurnMs: coldAgentTurn?.totalTurnMs ?? null,
warmAgentTurnMs: warmAgentTurn?.totalTurnMs ?? null,
agentColdWarmDeltaMs: delta(coldAgentTurn?.totalTurnMs, warmAgentTurn?.totalTurnMs),
@ -981,6 +991,7 @@ function collectAgentTurns(record, providerEvidence, scenario, timelineSummary,
: null;
const expectedFailureObserved = expectedFailure === true && result.status === 0 && result.timedOut !== true;
const normalResponseOk = result.status === 0 && result.timedOut !== true && response.usable === true && (expectedTextPresent !== false);
const isAgentCliTurn = isAgentCliMessageCommand(result.command);
const phaseBreakdown = buildAgentTurnBreakdown({ result: timingResult, attribution, timelineSummary, logSummary });
const turnDiagnostics = summarizeActiveTurnDiagnostics({
timelineSummary,
@ -998,6 +1009,16 @@ function collectAgentTurns(record, providerEvidence, scenario, timelineSummary,
timelineSummary
})
: null;
const agentCliPreProviderAttribution = isAgentCliTurn
? buildAgentCliPreProviderAttribution({
label: agentTurnLabel(phase.id, index),
phaseId: phase.id,
activeStartedAtEpochMs: timingResult.startedAtEpochMs,
activeFinishedAtEpochMs: timingResult.finishedAtEpochMs,
attribution,
timelineSummary
})
: null;
turns.push({
schemaVersion: "kova.agentTurnEvidence.v1",
index,
@ -1045,6 +1066,7 @@ function collectAgentTurns(record, providerEvidence, scenario, timelineSummary,
phaseBreakdown,
turnDiagnostics,
dashboardPreProviderAttribution,
agentCliPreProviderAttribution,
metadataScanCount: turnDiagnostics.metadataScan.count,
metadataScanTotalMs: turnDiagnostics.metadataScan.totalDurationMs,
metadataScanMaxMs: turnDiagnostics.metadataScan.maxDurationMs,
@ -1063,6 +1085,10 @@ function collectAgentTurns(record, providerEvidence, scenario, timelineSummary,
return turns;
}
function preferredPreProviderAttributionSummary(...summaries) {
return summaries.find((summary) => summary?.count > 0) ?? summaries[0];
}
function extractGatewaySessionTurn(result) {
if (!result?.command?.includes("run-dashboard-session-send-turn.mjs")) {
return null;
@ -3255,6 +3281,10 @@ function isAgentMessageCommand(command) {
command.includes("run-openai-compatible-turn.mjs");
}
function isAgentCliMessageCommand(command) {
return command.includes(" -- agent ") && command.includes("--message");
}
function extractAgentResponse(result) {
if (result.status !== 0 || result.timedOut) {
return { usable: false, text: null };

View File

@ -1,4 +1,5 @@
import { summarizeAgentTurnBreakdownForMarkdown } from "../collectors/agent-turns.mjs";
import { agentCliPreProviderMarkdownRows } from "../collectors/agent-cli-attribution.mjs";
import { dashboardPreProviderMarkdownRows } from "../collectors/dashboard-turn-attribution.mjs";
import { healthTotalFailures } from "../health.mjs";
@ -186,6 +187,9 @@ export function renderMarkdownReport(report) {
if (record.measurements.dashboardPreProviderAttribution?.count > 0) {
lines.push(`- Dashboard pre-provider known: cold ${record.measurements.coldPreProviderAttributedMs ?? "unknown"} ms; warm ${record.measurements.warmPreProviderAttributedMs ?? "unknown"} ms; unattributed cold ${record.measurements.coldPreProviderUnattributedMs ?? "unknown"} ms; warm ${record.measurements.warmPreProviderUnattributedMs ?? "unknown"} ms`);
}
if (record.measurements.agentCliPreProviderAttribution?.count > 0) {
lines.push(`- Agent CLI pre-provider known: cold ${record.measurements.coldPreProviderAttributedMs ?? "unknown"} ms; warm ${record.measurements.warmPreProviderAttributedMs ?? "unknown"} ms; unattributed cold ${record.measurements.coldPreProviderUnattributedMs ?? "unknown"} ms; warm ${record.measurements.warmPreProviderUnattributedMs ?? "unknown"} ms`);
}
lines.push(`- Agent provider final: cold ${record.measurements.coldProviderFinalMs ?? "unknown"} ms; warm ${record.measurements.warmProviderFinalMs ?? "unknown"} ms`);
lines.push(`- Agent turn stats: count ${record.measurements.agentTurnCount}; p95 ${record.measurements.agentTurnP95Ms ?? "unknown"} ms; max ${record.measurements.agentTurnMaxMs ?? "unknown"} ms; pre-provider p95 ${record.measurements.agentPreProviderP95Ms ?? "unknown"} ms`);
lines.push(`- Agent active-turn diagnostics: metadata scans ${record.measurements.agentMetadataScanCount ?? "unknown"} (${record.measurements.agentMetadataScanTotalMs ?? "unknown"} ms total); event-loop max ${record.measurements.agentEventLoopMaxMs ?? "unknown"} ms; session polls ${record.measurements.agentSessionPollCount ?? "unknown"} (${record.measurements.agentSessionPollErrorCount ?? "unknown"} errors)`);
@ -244,6 +248,7 @@ export function renderMarkdownReport(report) {
}
}
lines.push(...dashboardPreProviderMarkdownRows(record.measurements.agentTurns));
lines.push(...agentCliPreProviderMarkdownRows(record.measurements.agentTurns));
}
lines.push(`- Profiling: ${record.profiling?.enabled ? "enabled" : "off"} (${record.profiling?.interpretation ?? "unknown"})`);
lines.push(`- V8 reports / heap snapshots: ${record.measurements.v8ReportCount ?? "unknown"} / ${record.measurements.heapSnapshotCount ?? "unknown"}`);
@ -736,6 +741,7 @@ function summarizeMeasurements(measurements) {
agentSessionPollCount: measurements.agentSessionPollCount ?? null,
agentSessionPollErrorCount: measurements.agentSessionPollErrorCount ?? null,
dashboardPreProviderAttribution: measurements.dashboardPreProviderAttribution ?? null,
agentCliPreProviderAttribution: measurements.agentCliPreProviderAttribution ?? null,
coldPreProviderAttributedMs: measurements.coldPreProviderAttributedMs ?? null,
warmPreProviderAttributedMs: measurements.warmPreProviderAttributedMs ?? null,
coldPreProviderUnattributedMs: measurements.coldPreProviderUnattributedMs ?? null,
@ -1203,6 +1209,9 @@ function pushMeasurementBrief(lines, measurements, { compact }) {
if (measurements.dashboardPreProviderAttribution?.count > 0) {
lines.push(`- dashboard attribution: cold known ${valueMs(measurements.coldPreProviderAttributedMs)} / unattributed ${valueMs(measurements.coldPreProviderUnattributedMs)}; warm known ${valueMs(measurements.warmPreProviderAttributedMs)} / unattributed ${valueMs(measurements.warmPreProviderUnattributedMs)}`);
}
if (measurements.agentCliPreProviderAttribution?.count > 0) {
lines.push(`- agent CLI attribution: cold known ${valueMs(measurements.coldPreProviderAttributedMs)} / unattributed ${valueMs(measurements.coldPreProviderUnattributedMs)}; warm known ${valueMs(measurements.warmPreProviderAttributedMs)} / unattributed ${valueMs(measurements.warmPreProviderUnattributedMs)}`);
}
lines.push(`- plugins/runtime: missing deps ${measurements.missingDependencyErrors ?? "unknown"}; plugin failures ${measurements.pluginLoadFailures ?? "unknown"}; runtime deps ${valueMs(measurements.runtimeDepsStagingMs)}${runtimeDepsPluginText(measurements)}; warm restages ${measurements.warmRuntimeDepsRestageCount ?? "unknown"}; warm reuse ${measurements.runtimeDepsWarmReuseOk ?? "unknown"}`);
if (!compact || hasDiagnosticSignal(measurements)) {

View File

@ -31,6 +31,7 @@ import {
buildAgentTurnBreakdown,
summarizeAgentTurnBreakdownForMarkdown
} from "./collectors/agent-turns.mjs";
import { buildAgentCliPreProviderAttribution } from "./collectors/agent-cli-attribution.mjs";
import {
attributedSpanIntervals,
buildDashboardPreProviderAttribution
@ -378,6 +379,7 @@ export async function runSelfCheck(flags = {}) {
checks.push(agentTurnBreakdownCheck());
checks.push(gatewaySessionTurnEvaluationCheck());
checks.push(dashboardPreProviderAttributionCheck());
checks.push(agentCliPreProviderAttributionCheck());
checks.push(await mockProviderBehaviorCheck(tmp));
checks.push(providerFailureEvaluationCheck());
checks.push(agentColdWarmEvaluationCheck());
@ -2413,6 +2415,105 @@ function dashboardPreProviderAttributionCheck() {
}
}
function agentCliPreProviderAttributionCheck() {
try {
const base = 1777536000000;
const timelineText = [
timelineEvent({ type: "span.start", name: "agent.turn", timestamp: base + 1000, spanId: "cold-turn" }),
timelineEvent({ type: "span.start", name: "agent.prepare", timestamp: base + 1020, spanId: "cold-prepare" }),
timelineEvent({ type: "span.end", name: "agent.prepare", timestamp: base + 1120, spanId: "cold-prepare", durationMs: 100 }),
timelineEvent({ type: "span.start", name: "models.catalog.gateway", timestamp: base + 1080, spanId: "cold-models" }),
timelineEvent({ type: "span.end", name: "models.catalog.gateway", timestamp: base + 1180, spanId: "cold-models", durationMs: 100 }),
timelineEvent({ type: "span.start", name: "channel.plugin.load", timestamp: base + 1150, spanId: "cold-channel" }),
timelineEvent({ type: "span.error", name: "channel.plugin.load", timestamp: base + 1170, spanId: "cold-channel", durationMs: 20, errorName: "SyntheticError" }),
timelineEvent({ type: "span.end", name: "plugins.metadata.scan", timestamp: base + 1190, spanId: "cold-scan", durationMs: 30 }),
timelineEvent({ type: "provider.request", name: "provider.request", timestamp: base + 1200, receivedAtEpochMs: base + 1200, respondedAtEpochMs: base + 1700, durationMs: 500 }),
timelineEvent({ type: "span.end", name: "agent.turn", timestamp: base + 1900, spanId: "cold-turn", durationMs: 900 }),
timelineEvent({ type: "span.start", name: "runtimeDeps.stage", timestamp: base + 11020, spanId: "warm-runtime" }),
timelineEvent({ type: "span.end", name: "runtimeDeps.stage", timestamp: base + 11070, spanId: "warm-runtime", durationMs: 50 }),
timelineEvent({ type: "span.start", name: "channel.capabilities", timestamp: base + 11080, spanId: "warm-channel" }),
timelineEvent({ type: "span.end", name: "channel.capabilities", timestamp: base + 11110, spanId: "warm-channel", durationMs: 30 }),
timelineEvent({ type: "provider.request", name: "provider.request", timestamp: base + 11200, receivedAtEpochMs: base + 11200, respondedAtEpochMs: base + 11500, durationMs: 300 }),
timelineEvent({ type: "eventLoop.sample", name: "eventLoop.sample", timestamp: base + 11250, maxMs: 6 })
].join("\n");
const parsed = parseTimelineText(timelineText);
assertEqual(parsed.turnAttributionEvents.length, 16, "agent CLI turn attribution events retained");
const coldAttribution = buildAgentCliPreProviderAttribution({
label: "cold",
phaseId: "cold-agent-turn",
activeStartedAtEpochMs: base + 1000,
activeFinishedAtEpochMs: base + 1900,
attribution: {
firstProviderRequestAtEpochMs: base + 1200,
preProviderMs: 200,
providerFinalMs: 500
},
timelineSummary: {
available: true,
turnAttributionEvents: parsed.turnAttributionEvents,
artifacts: ["/tmp/kova/openclaw/timeline.jsonl"]
}
});
assertEqual(coldAttribution.available, true, "agent CLI cold attribution available");
assertEqual(coldAttribution.knownAttributedMs, 170, "agent CLI overlap-safe cold known attribution");
assertEqual(coldAttribution.unattributedMs, 30, "agent CLI cold unattributed remainder");
assertEqual(coldAttribution.spanSummaries.some((span) => span.name === "agent.turn"), false, "agent.turn parent span is not counted as pre-provider work");
assertEqual(coldAttribution.spanSummaries.find((span) => span.name === "channel.plugin.load")?.errorCount, 1, "agent CLI error span summary");
const missingAttribution = buildAgentCliPreProviderAttribution({
label: "cold",
phaseId: "cold-agent-turn",
activeStartedAtEpochMs: base + 1000,
activeFinishedAtEpochMs: base + 1900,
attribution: { firstProviderRequestAtEpochMs: base + 1200, preProviderMs: 200 },
timelineSummary: { available: false, artifacts: [] }
});
assertEqual(missingAttribution.available, false, "agent CLI missing timeline unavailable");
assertEqual(missingAttribution.unattributedMs, 200, "agent CLI missing timeline preserves full remainder");
const record = syntheticAgentCliRecord({ base, timeline: parsed });
evaluateRecord(record, {
id: "agent-cold-warm-message",
agent: { expectedText: "KOVA_AGENT_OK" },
thresholds: { agentTurnMs: 2000, coldAgentTurnMs: 2000, warmAgentTurnMs: 1000 }
}, { surface: { thresholds: {} }, targetPlan: { kind: "runtime" } });
assertEqual(record.measurements.agentCliPreProviderAttribution.count, 2, "record agent CLI attribution count");
assertEqual(record.measurements.dashboardPreProviderAttribution.count, 0, "record dashboard attribution stays empty for CLI turns");
assertEqual(record.measurements.coldPreProviderAttributedMs, 170, "record agent CLI cold attributed metric");
assertEqual(record.measurements.warmPreProviderAttributedMs, 80, "record agent CLI warm attributed metric");
assertEqual(record.measurements.warmPreProviderUnattributedMs, 120, "record agent CLI warm unattributed metric");
assertEqual(record.measurements.agentTurns[0].agentCliPreProviderAttribution.timelineArtifacts[0], "/tmp/kova/openclaw/timeline.jsonl", "record agent CLI timeline artifact");
const rendered = renderMarkdownReport({
generatedAt: "2026-05-01T00:00:00.000Z",
runId: "self-check-agent-cli-pre-provider",
mode: "self-check",
target: "runtime:stable",
platform: { os: "test", release: "test", arch: "test", node: "test" },
records: [record],
summary: { statuses: { PASS: 1 } }
});
assertEqual(rendered.includes("Agent CLI pre-provider attribution:"), true, "markdown includes agent CLI attribution table");
assertEqual(rendered.includes("`channel.plugin.load`"), true, "markdown includes agent CLI span table");
return {
id: "agent-cli-pre-provider-attribution",
status: "PASS",
command: "evaluate synthetic agent CLI pre-provider timeline attribution",
durationMs: 0
};
} catch (error) {
return {
id: "agent-cli-pre-provider-attribution",
status: "FAIL",
command: "evaluate synthetic agent CLI pre-provider timeline attribution",
durationMs: 0,
message: error.message
};
}
}
function timelineEvent(event) {
const timestamp = typeof event.timestamp === "number" ? new Date(event.timestamp).toISOString() : event.timestamp;
return JSON.stringify({
@ -2529,6 +2630,91 @@ function syntheticDashboardSessionRecord({ base, timeline }) {
};
}
function syntheticAgentCliRecord({ base, timeline }) {
return {
scenario: "agent-cold-warm-message",
surface: "agent-cold-warm-message",
title: "Agent CLI cold/warm",
status: "PASS",
cleanup: "done",
auth: { mode: "mock" },
phases: [
syntheticAgentCliTurnPhase({
id: "cold-agent-turn",
startedAtEpochMs: base + 1000,
finishedAtEpochMs: base + 1900
}),
syntheticAgentCliTurnPhase({
id: "warm-agent-turn",
startedAtEpochMs: base + 11000,
finishedAtEpochMs: base + 11600
})
],
providerEvidence: {
available: true,
requestCount: 2,
requests: [
{
requestId: "cold-provider",
receivedAt: new Date(base + 1200).toISOString(),
receivedAtEpochMs: base + 1200,
respondedAt: new Date(base + 1700).toISOString(),
respondedAtEpochMs: base + 1700,
firstByteLatencyMs: 20,
firstChunkLatencyMs: 25,
route: "/v1/responses",
model: "gpt-5.5",
status: 200
},
{
requestId: "warm-provider",
receivedAt: new Date(base + 11200).toISOString(),
receivedAtEpochMs: base + 11200,
respondedAt: new Date(base + 11500).toISOString(),
respondedAtEpochMs: base + 11500,
firstByteLatencyMs: 18,
firstChunkLatencyMs: 20,
route: "/v1/responses",
model: "gpt-5.5",
status: 200
}
]
},
finalMetrics: {
service: { gatewayState: "running" },
logs: zeroLogMetrics(),
timeline: {
...timeline,
artifacts: ["/tmp/kova/openclaw/timeline.jsonl"]
}
}
};
}
function syntheticAgentCliTurnPhase({ id, startedAtEpochMs, finishedAtEpochMs }) {
const command = "ocm @kova -- agent --local --agent main --session-id kova-agent-cold-warm --message hi --json";
return {
id,
title: id,
intent: "Synthetic agent CLI turn",
commands: [command],
evidence: [],
results: [{
command,
status: 0,
timedOut: false,
startedAt: new Date(startedAtEpochMs).toISOString(),
startedAtEpochMs,
finishedAt: new Date(finishedAtEpochMs).toISOString(),
finishedAtEpochMs,
durationMs: finishedAtEpochMs - startedAtEpochMs,
stdout: "{\"finalAssistantVisibleText\":\"KOVA_AGENT_OK\"}",
stderr: ""
}],
metrics: { logs: zeroLogMetrics(), health: { ok: true } }
};
}
function syntheticDashboardTurnPhase({ id, command, startedAtEpochMs, finishedAtEpochMs, payload }) {
return {
id,