Skip to content

Commit b13af38

Browse files
authored
perf(ui): trace chat first output latency
Add chat-send first visible assistant output telemetry in the Control UI, plus Gateway diagnostics correlation attributes for chat.send dispatch spans. Verified with focused UI/Gateway tests, tsgo, oxlint, autoreview, PR checks, and Testbox-through-Crabbox check:changed.
1 parent 4094c94 commit b13af38

6 files changed

Lines changed: 406 additions & 34 deletions

File tree

src/gateway/server-methods/chat.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2943,6 +2943,7 @@ export const chatHandlers: GatewayRequestHandlers = {
29432943
}
29442944
const rawSessionKey = p.sessionKey;
29452945
const agentIdOverride = normalizeOptionalText(p.agentId);
2946+
const clientRunId = p.idempotencyKey;
29462947
const requestedAgentId = resolveRequestedChatAgentId({
29472948
cfg: (context as { getRuntimeConfig?: () => OpenClawConfig }).getRuntimeConfig?.(),
29482949
requestedSessionKey: rawSessionKey,
@@ -2959,6 +2960,7 @@ export const chatHandlers: GatewayRequestHandlers = {
29592960
{
29602961
phase: "agent-turn",
29612962
attributes: {
2963+
runId: clientRunId,
29622964
hasAttachments: normalizedAttachments.length > 0,
29632965
hasExplicitOrigin: explicitOriginResult.value !== undefined,
29642966
},
@@ -3013,7 +3015,6 @@ export const chatHandlers: GatewayRequestHandlers = {
30133015
overrideMs: p.timeoutMs,
30143016
});
30153017
const now = Date.now();
3016-
const clientRunId = p.idempotencyKey;
30173018

30183019
const sendPolicy = resolveSendPolicy({
30193020
cfg,
@@ -3347,6 +3348,11 @@ export const chatHandlers: GatewayRequestHandlers = {
33473348
channel: INTERNAL_MESSAGE_CHANNEL,
33483349
});
33493350
const chatSendTraceAttributes = {
3351+
runId: clientRunId,
3352+
sessionKey,
3353+
agentId: selectedAgent.agentId ?? agentId,
3354+
provider: resolvedSessionModel.provider,
3355+
model: resolvedSessionModel.model,
33503356
hasAttachments: normalizedAttachments.length > 0,
33513357
hasExplicitOrigin: explicitOriginResult.value !== undefined,
33523358
hasConnectedClient: client?.connect !== undefined,

src/gateway/server.chat.gateway-server-chat-b.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,15 @@ async function writeMainSessionTranscript(sessionDir: string, lines: string[]) {
116116
await fs.writeFile(path.join(sessionDir, "sess-main.jsonl"), `${lines.join("\n")}\n`, "utf-8");
117117
}
118118

119+
async function readTimelineEvents(filePath: string): Promise<Array<Record<string, unknown>>> {
120+
const raw = await fs.readFile(filePath, "utf-8");
121+
return raw
122+
.trim()
123+
.split("\n")
124+
.filter(Boolean)
125+
.map((line) => JSON.parse(line) as Record<string, unknown>);
126+
}
127+
119128
async function fetchHistoryMessages(
120129
ws: GatewaySocket,
121130
params?: {
@@ -1178,6 +1187,60 @@ describe("gateway server chat", () => {
11781187
});
11791188
});
11801189

1190+
test("chat.send diagnostics timeline carries run correlation attributes", async () => {
1191+
const timelineDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-chat-timeline-"));
1192+
const timelinePath = path.join(timelineDir, "timeline.jsonl");
1193+
const previousDiagnostics = process.env.OPENCLAW_DIAGNOSTICS;
1194+
const previousTimelinePath = process.env.OPENCLAW_DIAGNOSTICS_TIMELINE_PATH;
1195+
process.env.OPENCLAW_DIAGNOSTICS = "timeline";
1196+
process.env.OPENCLAW_DIAGNOSTICS_TIMELINE_PATH = timelinePath;
1197+
try {
1198+
await withGatewayChatHarness(async ({ ws, createSessionDir }) => {
1199+
const spy = getReplyFromConfig;
1200+
await connectOk(ws);
1201+
1202+
await createSessionDir();
1203+
await writeMainSessionStore();
1204+
mockGetReplyFromConfigOnce(async () => undefined);
1205+
1206+
const sendRes = await rpcReq(ws, "chat.send", {
1207+
sessionKey: "main",
1208+
message: "hello",
1209+
idempotencyKey: "idem-timeline",
1210+
});
1211+
expect(sendRes.ok).toBe(true);
1212+
1213+
await vi.waitFor(() => {
1214+
expect(spy.mock.calls.length).toBeGreaterThan(0);
1215+
}, FAST_WAIT_OPTS);
1216+
await vi.waitFor(async () => {
1217+
const events = await readTimelineEvents(timelinePath);
1218+
expect(
1219+
events.some(
1220+
(event) =>
1221+
event.type === "span.end" &&
1222+
event.name === "gateway.chat_send.dispatch_inbound" &&
1223+
(event.attributes as Record<string, unknown> | undefined)?.runId ===
1224+
"idem-timeline",
1225+
),
1226+
).toBe(true);
1227+
}, FAST_WAIT_OPTS);
1228+
});
1229+
} finally {
1230+
if (previousDiagnostics === undefined) {
1231+
delete process.env.OPENCLAW_DIAGNOSTICS;
1232+
} else {
1233+
process.env.OPENCLAW_DIAGNOSTICS = previousDiagnostics;
1234+
}
1235+
if (previousTimelinePath === undefined) {
1236+
delete process.env.OPENCLAW_DIAGNOSTICS_TIMELINE_PATH;
1237+
} else {
1238+
process.env.OPENCLAW_DIAGNOSTICS_TIMELINE_PATH = previousTimelinePath;
1239+
}
1240+
await fs.rm(timelineDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
1241+
}
1242+
});
1243+
11811244
test("chat.history hard-caps single oversized nested payloads", async () => {
11821245
await withGatewayChatHarness(async ({ ws, createSessionDir }) => {
11831246
const historyMaxBytes = 64 * 1024;

0 commit comments

Comments
 (0)